Feat v0.7.3 extension shell migration
All checks were successful
CI/CD / detect-changes (pull_request) Successful in 4s
CI/CD / test-runners (pull_request) Has been skipped
CI/CD / test-frontend (pull_request) Successful in 7s
CI/CD / test-sqlite (pull_request) Successful in 2m48s
CI/CD / test-go-pg (pull_request) Successful in 2m53s
CI/CD / build-and-deploy (pull_request) Successful in 1m8s

Migrate Chat, Notes, and Schedules from legacy sw.shell.Topbar to the
v0.7.0 shell topbar contract (sw.shell.topbar.setTitle/setSlot),
eliminating double topbars on all extension surfaces.

- Chat v0.3.0: reactive slot for thread title + People button
- Notes v0.9.0: slot for New/Import/Graph buttons
- Schedules v0.2.0: reactive slot for count + New button
- 6 new shell-topbar runner tests (2 per surface)
- Headless E2E deferred to v0.7.5

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-02 12:52:40 +00:00
parent d6c7b21713
commit 4a6109b74f
18 changed files with 188 additions and 58 deletions

View File

@@ -2,6 +2,27 @@
All notable changes to Armature are documented here. 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 ## v0.7.1 — Surface Runner Framework
**`sw.testing` SDK Module** **`sw.testing` SDK Module**

View File

@@ -1,6 +1,6 @@
# Armature — Roadmap # Armature — Roadmap
## Current: v0.7.2Package Runners + CI Gate ## Current: v0.7.3Extension Shell Migration
Self-hosted extensible platform. Auth, identity, packages, Starlark sandbox, Self-hosted extensible platform. Auth, identity, packages, Starlark sandbox,
storage, realtime, and ops are kernel primitives. Everything else is an extension. 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 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. | | 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 ### v0.7.3 — Extension Shell Migration
**Extension Shell Migration**
Chat, Notes, and Schedules still use the old `sw.shell.Topbar` component, Chat, Notes, and Schedules still use the old `sw.shell.Topbar` component,
producing a double topbar (shell-injected + surface-owned). Migrate all producing a double topbar (shell-injected + surface-owned). Migrate all
@@ -163,20 +161,10 @@ same pattern as the kernel surface migrations.
| Step | Status | Description | | Step | Status | Description |
|------|--------|-------------| |------|--------|-------------|
| Chat → shell topbar | | Delete `<Topbar>`, use `setTitle('Chat')` + `setSlot()` for thread title/people button. | | Chat → shell topbar | done | Delete `<Topbar>`, use `setTitle('Chat')` + `setSlot()` for thread title/people button. |
| Notes → shell topbar | | Delete `<Topbar>`, use `setTitle('Notes')` + `setSlot()` for sidebar tabs/search. | | Notes → shell topbar | done | Delete `<Topbar>`, use `setTitle('Notes')` + `setSlot()` for action buttons. |
| Schedules → shell topbar | | Delete `<Topbar>`, use `setTitle('Schedules')`. Pattern A (title only). | | Schedules → shell topbar | done | Delete `<Topbar>`, use `setTitle('Schedules')` + `setSlot()` for count/new button. |
| Update package runner tests | | Assert shell topbar present, no double topbar, on migrated surfaces. | | Update package runner tests | done | Shell-topbar test suite in each runner — assert no legacy Topbar, uses shell API. |
**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. |
### v0.7.4 — Documentation + Deferred Surface Work ### 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. | | 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. | | 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 ## Post-v0.7.x

View File

@@ -1 +1 @@
0.7.2 0.7.3

View File

@@ -35,7 +35,8 @@
var modules = [ var modules = [
'conversations.js', 'conversations.js',
'messaging.js' 'messaging.js',
'shell-topbar.js'
]; ];
var loaded = 0; var loaded = 0;

View File

@@ -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');
});
});
})();

View File

@@ -4,6 +4,6 @@
"type": "test-runner", "type": "test-runner",
"title": "Chat Runner", "title": "Chat Runner",
"auth": "admin", "auth": "admin",
"version": "0.1.0", "version": "0.2.0",
"description": "Integration tests for Chat package — conversations, messaging, search." "description": "Integration tests for Chat package — conversations, messaging, search."
} }

View File

@@ -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: * Messaging surface built on chat-core library:
* sw.api.ext('chat-core') — conversation/message CRUD * sw.api.ext('chat-core') — conversation/message CRUD
* sw.api.ext('chat') — typing indicators * sw.api.ext('chat') — typing indicators
* sw.realtime — live events * sw.realtime — live events
* sw.ui.* — primitive components * sw.ui.* — primitive components
* sw.shell.Topbar — navigation bar * sw.shell.topbar shell topbar API
*/ */
(async function () { (async function () {
'use strict'; 'use strict';
@@ -42,7 +42,6 @@
var api = sw.api.ext('chat-core'); var api = sw.api.ext('chat-core');
var chatApi = sw.api.ext('chat'); var chatApi = sw.api.ext('chat');
var { Button, Spinner, Avatar, Dialog, Tabs } = sw.ui; var { Button, Spinner, Avatar, Dialog, Tabs } = sw.ui;
var Topbar = sw.shell.Topbar;
// Import UserPicker directly (not in sw.ui index) // Import UserPicker directly (not in sw.ui index)
var { UserPicker } = await import(base + '/js/sw/primitives/user-picker.js?v=' + ver); 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 selectedConv = conversations.find(c => c.id === selectedId);
var threadTitle = selectedConv ? (selectedConv.title || 'Direct Message') : ''; 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`
<span class="ext-chat-topbar__thread-title">${threadTitle}</span>
<${Button} size="sm" variant="secondary"
onClick=${() => setShowParticipants(!showParticipants)}>
${showParticipants ? 'Hide' : 'People'}
<//>
`);
} else {
sw.shell.topbar.setSlot(null);
}
}, [selectedId, threadTitle, showParticipants]);
if (loading) { if (loading) {
return html`<div class="ext-chat-loading"><${Spinner} /></div>`; return html`<div class="ext-chat-loading"><${Spinner} /></div>`;
} }
return html` return html`
<div class="ext-chat-app"> <div class="ext-chat-app">
<${Topbar} title="Chat">
${selectedId && html`
<span class="ext-chat-topbar__thread-title">${threadTitle}</span>
<${Button} size="sm" variant="secondary"
onClick=${() => setShowParticipants(!showParticipants)}>
${showParticipants ? 'Hide' : 'People'}
<//>`}
<//>
<div class="ext-chat-body"> <div class="ext-chat-body">
<${ConversationList} <${ConversationList}
selected=${selectedId} selected=${selectedId}

View File

@@ -6,7 +6,7 @@
"route": "/s/chat", "route": "/s/chat",
"auth": "authenticated", "auth": "authenticated",
"layout": "single", "layout": "single",
"version": "0.2.0", "version": "0.3.0",
"icon": "\ud83d\udcac", "icon": "\ud83d\udcac",
"description": "Chat surface — conversations, messaging, typing indicators, read receipts.", "description": "Chat surface — conversations, messaging, typing indicators, read receipts.",
"author": "armature", "author": "armature",

View File

@@ -36,7 +36,8 @@
var modules = [ var modules = [
'notes-crud.js', 'notes-crud.js',
'folders.js', 'folders.js',
'tags-search.js' 'tags-search.js',
'shell-topbar.js'
]; ];
var loaded = 0; var loaded = 0;

View File

@@ -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');
});
});
})();

View File

@@ -4,6 +4,6 @@
"type": "test-runner", "type": "test-runner",
"title": "Notes Runner", "title": "Notes Runner",
"auth": "admin", "auth": "admin",
"version": "0.1.0", "version": "0.2.0",
"description": "Integration tests for Notes package — CRUD, folders, tags, backlinks, search." "description": "Integration tests for Notes package — CRUD, folders, tags, backlinks, search."
} }

View File

@@ -1,10 +1,10 @@
/** /**
* Notes — Surface Entry Point (v0.8.0) * Notes — Surface Entry Point (v0.9.0)
* *
* Markdown notes surface using the SDK: * Markdown notes surface using the SDK:
* sw.api.ext('notes') — scoped API client * sw.api.ext('notes') — scoped API client
* sw.ui.* — primitive components * sw.ui.* — primitive components
* sw.shell.Topbar — navigation bar * sw.shell.topbar shell topbar API
* window.CM — CodeMirror 6 bundle (optional) * window.CM — CodeMirror 6 bundle (optional)
*/ */
(async function () { (async function () {
@@ -59,7 +59,6 @@
// ── SDK modules ──────────────────────────── // ── SDK modules ────────────────────────────
var api = sw.api.ext('notes'); var api = sw.api.ext('notes');
var { Button, Spinner } = sw.ui; var { Button, Spinner } = sw.ui;
var Topbar = sw.shell.Topbar;
// ── Debounce helper ──────────────────────── // ── Debounce helper ────────────────────────
var _timers = {}; var _timers = {};
@@ -1729,13 +1728,19 @@
debounce('search', function() { loadNotes(); }, 300); debounce('search', function() { loadNotes(); }, 300);
} }
return html` // ── Shell topbar ───────────────────────────
<${Topbar} title="Notes"> 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="primary" size="sm" onClick=${handleNew}>+ New Note<//>
<${Button} variant="secondary" size="sm" onClick=${handleImport}>Import .md<//> <${Button} variant="secondary" size="sm" onClick=${handleImport}>Import .md<//>
<${Button} variant=${showGraph ? 'primary' : 'secondary'} size="sm" <${Button} variant=${showGraph ? 'primary' : 'secondary'} size="sm"
onClick=${function() { setShowGraph(!showGraph); }}>Graph<//> onClick=${function() { setShowGraph(!showGraph); }}>Graph<//>
<//> `);
}, [showGraph]);
return html`
<div class="ext-notes-app"> <div class="ext-notes-app">
<div class="ext-notes-sidebar"> <div class="ext-notes-sidebar">
<${SidebarTabs} activeTab=${sidebarTab} onTabChange=${setSidebarTab} noteSelected=${!!activeNote} /> <${SidebarTabs} activeTab=${sidebarTab} onTabChange=${setSidebarTab} noteSelected=${!!activeNote} />

View File

@@ -6,7 +6,7 @@
"route": "/s/notes", "route": "/s/notes",
"auth": "authenticated", "auth": "authenticated",
"layout": "single", "layout": "single",
"version": "0.8.0", "version": "0.9.0",
"icon": "📝", "icon": "📝",
"description": "Markdown notes surface with rich editor, folders, tags, backlinks, import/export, sidebar tabs, document outline, and note graph.", "description": "Markdown notes surface with rich editor, folders, tags, backlinks, import/export, sidebar tabs, document outline, and note graph.",
"author": "armature", "author": "armature",

View File

@@ -34,7 +34,8 @@
if (window.SR.base) assetBase = window.SR.base + assetBase; if (window.SR.base) assetBase = window.SR.base + assetBase;
var modules = [ var modules = [
'schedules-crud.js' 'schedules-crud.js',
'shell-topbar.js'
]; ];
var loaded = 0; var loaded = 0;

View File

@@ -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');
});
});
})();

View File

@@ -4,6 +4,6 @@
"type": "test-runner", "type": "test-runner",
"title": "Schedules Runner", "title": "Schedules Runner",
"auth": "admin", "auth": "admin",
"version": "0.1.0", "version": "0.2.0",
"description": "Integration tests for Schedules package — CRUD, run, enable/disable." "description": "Integration tests for Schedules package — CRUD, run, enable/disable."
} }

View File

@@ -5,7 +5,7 @@
* this surface talks directly to the core scheduled-tasks endpoints. * this surface talks directly to the core scheduled-tasks endpoints.
* *
* SDK usage: * SDK usage:
* sw.shell.Topbar — standard navigation bar * sw.shell.topbar — shell topbar API
* sw.ui.* — primitive components * sw.ui.* — primitive components
* sw.api.* — raw REST calls to /api/v1/schedules * sw.api.* — raw REST calls to /api/v1/schedules
*/ */
@@ -39,7 +39,6 @@
var { useState, useEffect, useCallback } = hooks; var { useState, useEffect, useCallback } = hooks;
var { render } = preact; var { render } = preact;
var { Button, FormField, Dialog, Spinner, Banner } = sw.ui; var { Button, FormField, Dialog, Spinner, Banner } = sw.ui;
var Topbar = sw.shell?.Topbar;
// ── API helpers (core kernel endpoints) ──── // ── API helpers (core kernel endpoints) ────
var API = '/api/v1/schedules'; var API = '/api/v1/schedules';
@@ -365,6 +364,16 @@
useEffect(function () { loadItems(); }, []); useEffect(function () { loadItems(); }, []);
// ── Shell topbar ───────────────────────────
useEffect(function () {
if (!sw.shell?.topbar) return;
sw.shell.topbar.setTitle('Schedules');
sw.shell.topbar.setSlot(html`
<span class="ext-schedules-count">${items.length} schedule${items.length !== 1 ? 's' : ''}</span>
<${Button} variant="primary" size="sm" onClick=${() => setEditing({})}>+ New Schedule<//>
`);
}, [items.length]);
function handleSave() { function handleSave() {
setEditing(null); setEditing(null);
loadItems(); loadItems();
@@ -372,17 +381,6 @@
return html` return html`
<div class="ext-schedules-app"> <div class="ext-schedules-app">
${Topbar ? html`
<${Topbar} title="Schedules">
<span class="ext-schedules-count">${items.length} schedule${items.length !== 1 ? 's' : ''}</span>
<${Button} variant="primary" size="sm" onClick=${() => setEditing({})}>+ New Schedule<//>
<//>
` : html`
<div class="ext-schedules-header">
<h1>Schedules</h1>
<${Button} variant="primary" size="sm" onClick=${() => setEditing({})}>+ New Schedule<//>
</div>
`}
${error && html`<${Banner} variant="danger" text=${error} />`} ${error && html`<${Banner} variant="danger" text=${error} />`}

View File

@@ -2,7 +2,7 @@
"id": "schedules", "id": "schedules",
"title": "Schedules", "title": "Schedules",
"type": "full", "type": "full",
"version": "0.1.0", "version": "0.2.0",
"tier": "browser", "tier": "browser",
"author": "Armature", "author": "Armature",
"icon": "⏰", "icon": "⏰",