mirror of
https://github.com/got-feedBack/feedBack.git
synced 2026-09-12 07:08:32 +00:00
Implement
This commit is contained in:
@@ -56,6 +56,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
- **Audio-input control plane** — makes `audio-input` the redaction-safe source of truth for instrument input discovery and lifecycle. Core now exposes `list-sources`, `select-source`, `open-source`, and `close-source` through the capability runtime, persists selected logical sources, keeps inspect/list/select prompt-free, routes provider `source.open`/`source.close` operations with bounded outcomes, shares compatible open sessions across requesters, suppresses compatibility-backed duplicate sources when a native provider owns the same logical key, and expands audio-session diagnostics/Capability Inspector rendering for selected input, open sessions, bridge hits, storage status, and permission/device failures without exposing raw device labels or live audio handles.
|
- **Audio-input control plane** — makes `audio-input` the redaction-safe source of truth for instrument input discovery and lifecycle. Core now exposes `list-sources`, `select-source`, `open-source`, and `close-source` through the capability runtime, persists selected logical sources, keeps inspect/list/select prompt-free, routes provider `source.open`/`source.close` operations with bounded outcomes, shares compatible open sessions across requesters, suppresses compatibility-backed duplicate sources when a native provider owns the same logical key, and expands audio-session diagnostics/Capability Inspector rendering for selected input, open sessions, bridge hits, storage status, and permission/device failures without exposing raw device labels or live audio handles.
|
||||||
- **Audio-monitoring control plane** — makes `audio-monitoring` the shared live-monitoring coordinator. Core now exposes provider registration/list/selection, explicit user-action `start`, requester-counted `stop`, prompt-free `inspect`/`monitoring.status`, and `set-direct-monitor` through the capability runtime. Monitoring starts integrate with selected `audio-input` readiness, background requesters can only attach to active compatible sessions, active sessions survive song/playback stops without auto-resuming after reload, native providers suppress compatibility-backed legacy monitor paths, and diagnostics/Capability Inspector now show providers, sessions, requesters, direct-monitor state, bridge hits, and distinct safe outcomes (`provider-selection-required`, `user-action-required`, `incompatible`, `unavailable`, `stopped`, etc.) without exposing raw audio/device data.
|
- **Audio-monitoring control plane** — makes `audio-monitoring` the shared live-monitoring coordinator. Core now exposes provider registration/list/selection, explicit user-action `start`, requester-counted `stop`, prompt-free `inspect`/`monitoring.status`, and `set-direct-monitor` through the capability runtime. Monitoring starts integrate with selected `audio-input` readiness, background requesters can only attach to active compatible sessions, active sessions survive song/playback stops without auto-resuming after reload, native providers suppress compatibility-backed legacy monitor paths, and diagnostics/Capability Inspector now show providers, sessions, requesters, direct-monitor state, bridge hits, and distinct safe outcomes (`provider-selection-required`, `user-action-required`, `incompatible`, `unavailable`, `stopped`, etc.) without exposing raw audio/device data.
|
||||||
- **Playback control plane** — promotes `playback` to an active core capability domain for song transport, timing, loop, route, requester/observer, bridge, and diagnostics state. Core now exposes `inspect`, user-authorized `start`, `pause`, `resume`, `stop`, `seek`, `set-loop`, and `clear-loop` through the capability runtime while `static/app.js` keeps raw `<audio>`/JUCE handles private behind a redaction-safe adapter. Playback diagnostics use pseudonymous targets in exported bundles, local display labels only in the Capability Inspector, bounded recent outcomes/events, and bridge accounting for `window.playSong`, legacy `song:*` events, `window.slopsmith` transport helpers, loop helpers, and browser/native route handoff.
|
- **Playback control plane** — promotes `playback` to an active core capability domain for song transport, timing, loop, route, requester/observer, bridge, and diagnostics state. Core now exposes `inspect`, user-authorized `start`, `pause`, `resume`, `stop`, `seek`, `set-loop`, and `clear-loop` through the capability runtime while `static/app.js` keeps raw `<audio>`/JUCE handles private behind a redaction-safe adapter. Playback diagnostics use pseudonymous targets in exported bundles, local display labels only in the Capability Inspector, bounded recent outcomes/events, and bridge accounting for `window.playSong`, legacy `song:*` events, `window.slopsmith` transport helpers, loop helpers, and browser/native route handoff.
|
||||||
|
- **Jobs control plane** — promotes `jobs` to an active privileged capability domain for long-running plugin work. Core now exposes provider registration, selected-provider persistence, approval-gated enqueue/retry, capacity-aware scheduling, prompt-free list/inspect, cancel/pause/resume/retry coordination, reload recovery for provider-declared references, compatibility bridge hits, and `slopsmith.jobs.diagnostics.v1` snapshots. Providers keep actual writes/downloads/subprocesses private; diagnostics redact paths, filenames, URLs, tokens, command lines, media/artifacts, recordings, live handles, and provider-private payloads.
|
||||||
- **3D highway — Tone HUD, fret dividers, chord-diagram toggle, FPS counter.** The bundled `plugins/highway_3d` gains an amber **Tone-change HUD** (shows the active tone and the next scheduled tone change; position / size / visibility configurable in settings), a **fret-dividers** toggle (vertical dividers on the highway, on by default, via `h3dBgSetFretDividersVisible`), a **chord-diagram visibility** toggle (`h3dBgSetChordDiagramVisible`), and an **FPS counter** setting migrated to `BG_DEFAULTS.fpsVisible` (drops the legacy `h3d_showFps` localStorage key). Chord-diagram position is restricted to `tl`/`tr`; legacy `bl`/`br` values are coerced on load. Perf: accent-halo shell descriptors are pre-built per string in `initScene()` and the chord-verdict cache key is encoded as a number, eliminating per-frame allocations in the `drawNote()` and chord hot paths.
|
- **3D highway — Tone HUD, fret dividers, chord-diagram toggle, FPS counter.** The bundled `plugins/highway_3d` gains an amber **Tone-change HUD** (shows the active tone and the next scheduled tone change; position / size / visibility configurable in settings), a **fret-dividers** toggle (vertical dividers on the highway, on by default, via `h3dBgSetFretDividersVisible`), a **chord-diagram visibility** toggle (`h3dBgSetChordDiagramVisible`), and an **FPS counter** setting migrated to `BG_DEFAULTS.fpsVisible` (drops the legacy `h3d_showFps` localStorage key). Chord-diagram position is restricted to `tl`/`tr`; legacy `bl`/`br` values are coerced on load. Perf: accent-halo shell descriptors are pre-built per string in `initScene()` and the chord-verdict cache key is encoded as a number, eliminating per-frame allocations in the `drawNote()` and chord hot paths.
|
||||||
- **Sloppak assembly preserves a short preview clip.** When a source chart carries a separate short browser-preview audio clip alongside the full song, the sloppak assembler now decodes it to `preview.ogg` at the sloppak root and records it under a new top-level `preview:` manifest key (POSIX relpath, same shape as `lyrics`/`cover`). A failed preview decode is logged at debug and skipped without aborting the overall build. Sources with no separate preview are unaffected. Older sloppak readers ignore the unknown `preview` key, so the change is purely additive (sloppak-spec.md §5.5 backward-compat). Documented in `docs/sloppak-spec.md` §2 alongside the other optional top-level keys. Enables [`slopsmith-plugin-song-preview`](https://github.com/got-feedback/feedback-plugin-song-preview) to render hover-to-listen previews for sloppaks without seeking into the full audio.
|
- **Sloppak assembly preserves a short preview clip.** When a source chart carries a separate short browser-preview audio clip alongside the full song, the sloppak assembler now decodes it to `preview.ogg` at the sloppak root and records it under a new top-level `preview:` manifest key (POSIX relpath, same shape as `lyrics`/`cover`). A failed preview decode is logged at debug and skipped without aborting the overall build. Sources with no separate preview are unaffected. Older sloppak readers ignore the unknown `preview` key, so the change is purely additive (sloppak-spec.md §5.5 backward-compat). Documented in `docs/sloppak-spec.md` §2 alongside the other optional top-level keys. Enables [`slopsmith-plugin-song-preview`](https://github.com/got-feedback/feedback-plugin-song-preview) to render hover-to-listen previews for sloppaks without seeking into the full audio.
|
||||||
- **Generic plugin asset route** — `GET /api/plugins/{plugin_id}/assets/{path}` serves arbitrary static files a plugin bundles under its own `assets/` directory (AudioWorklet modules, WASM, images, etc.), so plugins can self-host browser-fetchable assets without a CDN (Principle II). Containment is enforced by `lib/safepath.safe_join` against `<plugin>/assets/`, so `..` traversal, absolute paths, and NUL bytes cannot escape `assets/` to reach a plugin's Python modules. `.js` is served as `application/javascript`. First consumer: the stems plugin's pitch-preserving time-stretch worklet.
|
- **Generic plugin asset route** — `GET /api/plugins/{plugin_id}/assets/{path}` serves arbitrary static files a plugin bundles under its own `assets/` directory (AudioWorklet modules, WASM, images, etc.), so plugins can self-host browser-fetchable assets without a CDN (Principle II). Containment is enforced by `lib/safepath.safe_join` against `<plugin>/assets/`, so `..` traversal, absolute paths, and NUL bytes cannot escape `assets/` to reach a plugin's Python modules. `.js` is served as `application/javascript`. First consumer: the stems plugin's pitch-preserving time-stretch worklet.
|
||||||
|
|||||||
@@ -153,6 +153,18 @@ The legacy chart-coupled surface — `highway.setNoteStateProvider(fn)`, the sin
|
|||||||
|
|
||||||
Diagnostics live under `slopsmith.note_detection_capability.v1` and contain provider ids/labels/kinds, binding summaries (requester, provider, redacted context, target size), availability, and the last bounded outcome (event, binding, provider, MIDI number, hit flag) — never raw audio buffers, sample data, device labels, or song identity.
|
Diagnostics live under `slopsmith.note_detection_capability.v1` and contain provider ids/labels/kinds, binding summaries (requester, provider, redacted context, target size), availability, and the last bounded outcome (event, binding, provider, MIDI number, hit flag) — never raw audio buffers, sample data, device labels, or song identity.
|
||||||
|
|
||||||
|
## Jobs Control Plane
|
||||||
|
|
||||||
|
The jobs slice promotes `jobs` as a privileged core provider-coordinator implemented by [static/capabilities/jobs.js](../static/capabilities/jobs.js). It coordinates long-running plugin work such as conversion, import, cache-building, update, preview, and studio-style background tasks while providers keep the actual file writes, subprocesses, downloads, and private payloads inside their own code.
|
||||||
|
|
||||||
|
The public command surface is `register-provider`, `unregister-provider`, `list-providers`, `enqueue`, `list`, `inspect`, `cancel`, `pause`, `resume`, `retry`, and `record-bridge-hit`. Provider operations are `job.enqueue`, `job.status`, `job.cancel`, `job.pause`, `job.resume`, `job.retry`, and `job.recover`. `list` and `inspect` are prompt-free and side-effect-free: they never trigger provider work, writes, downloads, subprocesses, or external calls. Fresh privileged `enqueue` and `retry` requests require an explicit `authorization: "user-action"` or a matching approved-continuation scope before provider callbacks run.
|
||||||
|
|
||||||
|
Jobs are scheduled by provider capacity and priority (`user-approved-interactive` before `background-maintenance`). State transitions are explicit (`queued`, `running`, `paused`, `cancellation-requested`, terminal cancelled/completed/failed/provider-unavailable/orphaned), and outcomes use the shared canonical vocabulary: `handled`, `queued`, `denied`, `user-action-required`, `unavailable`, `no-owner`, `no-handler`, `no-target`, `unsupported-command`, `unsupported-operation`, `incompatible`, `incompatible-version`, `provider-selection-required`, `validation-failed`, `stale`, `cancelled`, `completed`, `failed`, `timeout`, and `retry-started`.
|
||||||
|
|
||||||
|
Diagnostics live under `slopsmith.jobs.diagnostics.v1`. Snapshots include provider summaries, selected/default providers, active/queued/paused/recent-terminal jobs, bounded per-job history, recent outcomes, bridge hits, limits, and notes. Browser persistence is limited to selected provider choices and provider-declared recoverable job references; it does not store raw payloads, non-recoverable active job state, file paths, command lines, URLs, tokens, media buffers, recordings, live handles, or provider-private data. On reload, only provider-declared recoverable jobs are restored; the rest become orphaned or provider-unavailable with a safe reason.
|
||||||
|
|
||||||
|
Compatibility bridge hits such as `jobs.legacy-plugin-queue`, `jobs.legacy-status-screen`, `jobs.legacy-backend-route`, `jobs.legacy-progress-poll`, and `jobs.legacy-update-flow` are diagnostics-only attribution. They never grant approval or execute queued work by themselves.
|
||||||
|
|
||||||
## Capability Roles
|
## Capability Roles
|
||||||
|
|
||||||
Use capability declarations for provider/requester/observer relationships:
|
Use capability declarations for provider/requester/observer relationships:
|
||||||
@@ -180,7 +192,9 @@ Core domains include review metadata in diagnostics:
|
|||||||
- `active`: wired to current Slopsmith behavior and expected to work as an integration point.
|
- `active`: wired to current Slopsmith behavior and expected to work as an integration point.
|
||||||
- `diagnostic`: support/inspection-only runtime surfaces.
|
- `diagnostic`: support/inspection-only runtime surfaces.
|
||||||
|
|
||||||
PR1 includes only the delivered domains listed in [capability-roadmap.md](capability-roadmap.md): `pipeline`, `diagnostics`, and `library`. The follow-up audio graph/session slice promotes `audio-mix`, `audio-input`, `audio-monitoring`, and a coordinated `stems` surface. The playback slice promotes `playback` as an active transport control plane. The audio-effects slice promotes provider-selected effect-chain planning while leaving physical processor loading to compatible executors such as trusted Desktop native audio or a browser/WASM executor. The visualization slice promotes `visualization` as the highway renderer provider-coordinator, and the note-detection slice (spec 009) promotes `note-detection` as the detection-binding control plane. Backend routes, app UI, settings, and other hardware-facing domains remain documented in the roadmap and safety matrix until their own host workflow/provider slice exists.
|
PR1 includes only the delivered domains listed in [capability-roadmap.md](capability-roadmap.md): `pipeline`, `diagnostics`, and `library`. The follow-up audio graph/session slice promotes `audio-mix`, `audio-input`, `audio-monitoring`, and a coordinated `stems` surface. The playback slice promotes `playback` as an active transport control plane. The audio-effects slice promotes provider-selected effect-chain planning while leaving physical processor loading to compatible executors such as trusted Desktop native audio or a browser/WASM executor. The visualization slice promotes `visualization` as the highway renderer provider-coordinator, the note-detection slice (spec 009) promotes `note-detection` as the detection-binding control plane, and the jobs slice promotes `jobs` as a privileged provider-coordinator for long-running work. Backend routes, app UI, settings, and other hardware-facing domains remain documented in the roadmap and safety matrix until their own host workflow/provider slice exists.
|
||||||
|
|
||||||
|
PR1 includes only the delivered domains listed in [capability-roadmap.md](capability-roadmap.md): `pipeline`, `diagnostics`, and `library`. The follow-up audio graph/session slice promotes `audio-mix`, `audio-input`, `audio-monitoring`, and a coordinated `stems` surface. The playback slice promotes `playback` as an active transport control plane. The audio-effects slice promotes provider-selected effect-chain planning while leaving physical processor loading to compatible executors such as trusted Desktop native audio or a browser/WASM executor. The jobs slice promotes `jobs` as a privileged provider-coordinator for long-running work. Backend routes, app UI, settings, visualization, note-detection, and other hardware-facing domains remain documented in the roadmap and safety matrix until their own host workflow/provider slice exists.
|
||||||
|
|
||||||
Capability metadata is versioned by the `capability-pipelines.v1` standard. Invalid roles, commands, operations, requests, observes, emits, events, owner kinds, compatibility modes, ownership policies, safety classes, or version fields are excluded from the capability graph and surfaced through `capability_validation_warnings`; legacy plugin fields continue to load through their existing app paths. Plugins that declare a future `capability-pipelines` version are reported through `capability_unsupported_versions` and their runtime handlers are marked incompatible.
|
Capability metadata is versioned by the `capability-pipelines.v1` standard. Invalid roles, commands, operations, requests, observes, emits, events, owner kinds, compatibility modes, ownership policies, safety classes, or version fields are excluded from the capability graph and surfaced through `capability_validation_warnings`; legacy plugin fields continue to load through their existing app paths. Plugins that declare a future `capability-pipelines` version are reported through `capability_unsupported_versions` and their runtime handlers are marked incompatible.
|
||||||
|
|
||||||
@@ -228,11 +242,13 @@ The compatibility ownership vocabulary remains:
|
|||||||
- `privileged`: command execution needs an explicit enforcement plan before shipping.
|
- `privileged`: command execution needs an explicit enforcement plan before shipping.
|
||||||
- `diagnostic-only`: read-only support and inspector surfaces.
|
- `diagnostic-only`: read-only support and inspector surfaces.
|
||||||
|
|
||||||
Dispatch results use explicit outcomes: `handled`, `transformed`, `denied`, `failed`, `degraded`, `short-circuited`, `overridden`, `no-owner`, `no-handler`, `no-target`, `unsupported-command`, `incompatible`, `incompatible-version`, `unavailable`, `provider-selection-required`, `user-action-required`, `stale`, `cancelled`, and `stopped`. No-owner, no-handler, no-target, unsupported-command, incompatible, incompatible-version, provider-selection-required, user-action-required, stale, and cancelled decisions are recorded in diagnostics so support bundles explain why nothing happened.
|
Dispatch results use explicit outcomes: `handled`, `transformed`, `denied`, `failed`, `degraded`, `short-circuited`, `overridden`, `no-owner`, `no-handler`, `no-target`, `unsupported-command`, `unsupported-operation`, `incompatible`, `incompatible-version`, `unavailable`, `provider-selection-required`, `user-action-required`, `stale`, `queued`, `validation-failed`, `cancelled`, `completed`, `timeout`, `retry-started`, and `stopped`. No-owner, no-handler, no-target, unsupported-command, unsupported-operation, incompatible, incompatible-version, provider-selection-required, user-action-required, stale, queued, validation-failed, cancelled, completed, timeout, and retry-started decisions are recorded in diagnostics so support bundles explain what happened.
|
||||||
|
|
||||||
## Deferred Core Adapters
|
## Deferred Core Adapters
|
||||||
|
|
||||||
UI placement and settings contributions are real Slopsmith surfaces, but they are not PR1 capability contracts (visualization is active as of the cap:6 slice; note-detection as of the spec-009 slice). Audio mixer/session domains are active as of the audio graph/session slice, playback is active as of the playback control-plane slice, and audio-effects is active as a provider/route/chain-plan coordinator; plugins should keep using current documented APIs for remaining areas until the corresponding domain PR ships the host workflow, command/event contract, compatibility shims, diagnostics fields, and tests.
|
UI placement and settings contributions are real Slopsmith surfaces, but they are not PR1 capability contracts (visualization is active as of the cap:6 slice, note-detection as of the spec-009 slice, and jobs as of the jobs control-plane slice). Audio mixer/session domains are active as of the audio graph/session slice, playback is active as of the playback control-plane slice, and audio-effects is active as a provider/route/chain-plan coordinator; plugins should keep using current documented APIs for remaining areas until the corresponding domain PR ships the host workflow, command/event contract, compatibility shims, diagnostics fields, and tests.
|
||||||
|
|
||||||
|
UI placement, settings contributions, visualization, and note-detection are real Slopsmith surfaces, but they are not PR1 capability contracts. Audio mixer/session domains are active as of the audio graph/session slice, playback is active as of the playback control-plane slice, audio-effects is active as a provider/route/chain-plan coordinator, and jobs are active as of the jobs control-plane slice; plugins should keep using current documented APIs for remaining areas until the corresponding domain PR ships the host workflow, command/event contract, compatibility shims, diagnostics fields, and tests.
|
||||||
|
|
||||||
The library provider workflow is the PR1 core adapter and is implemented natively as the `library` capability module. Provider refresh, selection, and sync run through `library` owner commands; backend provider registration remains the way providers enter the library registry, and the browser module turns that registry into provider participants. The app event bus continues to dispatch local `window.slopsmith` events for legacy listeners; playback now mirrors song transport, route, seek, and loop lifecycle into `playback`, and visualization attributes renderer selection/failure, while navigation, note, and route-only surfaces remain outside capability domains until their own slices land.
|
The library provider workflow is the PR1 core adapter and is implemented natively as the `library` capability module. Provider refresh, selection, and sync run through `library` owner commands; backend provider registration remains the way providers enter the library registry, and the browser module turns that registry into provider participants. The app event bus continues to dispatch local `window.slopsmith` events for legacy listeners; playback now mirrors song transport, route, seek, and loop lifecycle into `playback`, and visualization attributes renderer selection/failure, while navigation, note, and route-only surfaces remain outside capability domains until their own slices land.
|
||||||
|
|
||||||
@@ -283,6 +299,7 @@ From the `slopsmith/` directory:
|
|||||||
```bash
|
```bash
|
||||||
node --check static/app.js
|
node --check static/app.js
|
||||||
node --check static/capabilities.js
|
node --check static/capabilities.js
|
||||||
|
node --check static/capabilities/jobs.js
|
||||||
node --check static/diagnostics.js
|
node --check static/diagnostics.js
|
||||||
node --check plugins/capability_inspector/screen.js
|
node --check plugins/capability_inspector/screen.js
|
||||||
node --test tests/js/*.test.js
|
node --test tests/js/*.test.js
|
||||||
|
|||||||
@@ -122,6 +122,71 @@ A route-only wrapper that uses the library capability without registering a brow
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Jobs Provider And Requester
|
||||||
|
|
||||||
|
Plugins that run long-lived work should register as `jobs` providers and keep actual work private. The jobs host only stores safe summaries, provider ids, selected-provider choices, progress, terminal outcomes, and provider-declared recovery references.
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": "sloppak_converter",
|
||||||
|
"name": "Sloppak Converter",
|
||||||
|
"standards": ["capability-pipelines.v1", "plugin-runtime-idempotent.v1"],
|
||||||
|
"capabilities": {
|
||||||
|
"jobs": {
|
||||||
|
"roles": ["provider", "observer"],
|
||||||
|
"operations": ["job.enqueue", "job.status", "job.cancel", "job.retry", "job.recover"],
|
||||||
|
"events": ["queued", "started", "progress", "completed", "failed", "cancelled"],
|
||||||
|
"mode": "active",
|
||||||
|
"compatibility": "shim-allowed",
|
||||||
|
"ownership": "multi-provider",
|
||||||
|
"safety": "privileged",
|
||||||
|
"version": 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Runtime registration supplies the redaction-safe provider summary and operation callbacks:
|
||||||
|
|
||||||
|
```js
|
||||||
|
window.slopsmith.jobs.registerProvider({
|
||||||
|
providerId: 'sloppak_converter.jobs',
|
||||||
|
pluginId: 'sloppak_converter',
|
||||||
|
label: 'Sloppak Converter',
|
||||||
|
jobTypes: ['psarc-to-sloppak'],
|
||||||
|
actions: ['enqueue', 'inspect', 'cancel', 'retry', 'recover'],
|
||||||
|
capacity: { maxRunning: 1, maxQueued: 20 },
|
||||||
|
recoverySupport: { queued: true, running: false, paused: false },
|
||||||
|
operationHandlers: {
|
||||||
|
'job.enqueue': ({ job }) => startConversion(job),
|
||||||
|
'job.cancel': ({ job }) => cancelConversion(job.jobId),
|
||||||
|
'job.retry': ({ job }) => startConversion(job),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
Requesters enqueue privileged work only from a user action or matching approved continuation. Use safe labels and fingerprints rather than paths, filenames, URLs, command lines, or raw payloads.
|
||||||
|
|
||||||
|
```js
|
||||||
|
const result = await window.slopsmith.capabilities.dispatch({
|
||||||
|
capability: 'jobs',
|
||||||
|
command: 'enqueue',
|
||||||
|
source: 'sloppak_converter',
|
||||||
|
args: {
|
||||||
|
jobType: 'psarc-to-sloppak',
|
||||||
|
requester: 'sloppak_converter',
|
||||||
|
authorization: 'user-action',
|
||||||
|
target: { targetRef: 'song-target-abc123' },
|
||||||
|
inputs: { safeFingerprint: 'input-fingerprint-abc123' },
|
||||||
|
safeLabel: 'Convert selected song',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result.outcome === 'user-action-required') {
|
||||||
|
// Show an explicit button; do not start provider work in the background.
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
## Audio Mix Fader Provider
|
## Audio Mix Fader Provider
|
||||||
|
|
||||||
Existing plugins can keep using `window.slopsmith.audio.registerFader(spec)` while migrating. The compatibility bridge records the fader as an `audio-mix` participant. New bundled code should prefer a native participant declaration plus the audio-session helper once available in its integration point.
|
Existing plugins can keep using `window.slopsmith.audio.registerFader(spec)` while migrating. The compatibility bridge records the fader as an `audio-mix` participant. New bundled code should prefer a native participant declaration plus the audio-session helper once available in its integration point.
|
||||||
|
|||||||
@@ -60,6 +60,14 @@ The progression slice (spec 010) promotes `progression` as an active exclusive-o
|
|||||||
|
|
||||||
Deferred follow-up slices: a `contributor` role so plugins ship their own challenge/quest content (drums challenges from a drums-scoring plugin, quest-pool entries from minigame plugins), and drums scoring wiring so `song_completed {instrument: "drums"}` goals become satisfiable.
|
Deferred follow-up slices: a `contributor` role so plugins ship their own challenge/quest content (drums challenges from a drums-scoring plugin, quest-pool entries from minigame plugins), and drums scoring wiring so `song_completed {instrument: "drums"}` goals become satisfiable.
|
||||||
|
|
||||||
|
## Jobs Control Plane Slice
|
||||||
|
|
||||||
|
The jobs slice promotes `jobs` from a deferred domain to an active privileged provider-coordinator. It owns provider registration, selected-provider persistence, capacity-aware scheduling, explicit user-approved enqueue/retry gates, prompt-free list/inspect, cancellation/pause/resume/retry coordination, reload recovery for provider-declared recoverable references, compatibility bridge hit accounting, and redaction-safe diagnostics under `slopsmith.jobs.diagnostics.v1`.
|
||||||
|
|
||||||
|
Providers keep actual privileged work private. Core stores only safe job summaries, provider metadata, selected/default provider choices, bounded lifecycle history, terminal outcomes, and recovery references. It does not persist raw payloads, active non-recoverable work, DB schemas, paths, filenames, URLs, tokens, command lines, media/artifacts, recordings, live handles, or provider-private values.
|
||||||
|
|
||||||
|
Jobs bridge removal gates are: bundled and first-party long-running workflows use native `jobs` provider registration/dispatch; normal conversion/import/update/cache smoke runs show no unexpected legacy bridge hits; diagnostics distinguish queued, denied, user-action-required, provider-selection-required, stale, cancelled, completed, failed, timeout, retry-started, orphaned, and provider-unavailable cases; repeated plugin hydration does not duplicate providers or jobs; and reload recovery restores only provider-declared safe references.
|
||||||
|
|
||||||
## Recommended Next Slices
|
## Recommended Next Slices
|
||||||
|
|
||||||
The plugin inventory suggests this migration order after the audio graph/session and playback slices:
|
The plugin inventory suggests this migration order after the audio graph/session and playback slices:
|
||||||
@@ -69,6 +77,10 @@ The plugin inventory suggests this migration order after the audio graph/session
|
|||||||
3. UI contribution host: migrate navigation, plugin screens, player controls, player panels, overlays, shortcuts, and guided tours under placement/lifecycle policy.
|
3. UI contribution host: migrate navigation, plugin screens, player controls, player panels, overlays, shortcuts, and guided tours under placement/lifecycle policy.
|
||||||
4. Backend and privileged capability cleanup: migrate routes, plugin lifecycle, media import/export, recording, external services, and subprocess-backed workflows with explicit user confirmation and diagnostics redaction.
|
4. Backend and privileged capability cleanup: migrate routes, plugin lifecycle, media import/export, recording, external services, and subprocess-backed workflows with explicit user confirmation and diagnostics redaction.
|
||||||
|
|
||||||
|
1. `note-detection`: formalize note-state providers, calibration diagnostics, audio-input coupling, and hit/miss event flow. Requesters should use `audio-input` for source selection/opening rather than owning device prompts directly.
|
||||||
|
2. UI contribution host: migrate navigation, plugin screens, player controls, player panels, overlays, shortcuts, and guided tours under placement/lifecycle policy.
|
||||||
|
3. Backend and privileged capability cleanup: migrate routes, plugin lifecycle, media import/export, recording, external services, and subprocess-backed workflows with explicit user confirmation and diagnostics redaction.
|
||||||
|
|
||||||
## UI/UX Migration Path
|
## UI/UX Migration Path
|
||||||
|
|
||||||
This is the recommended order for UI/UX capability work only. It excludes audio semantics, backend route execution, media jobs, and plugin install/update behavior except where those systems need a visible contribution point.
|
This is the recommended order for UI/UX capability work only. It excludes audio semantics, backend route execution, media jobs, and plugin install/update behavior except where those systems need a visible contribution point.
|
||||||
@@ -107,7 +119,6 @@ These domains are planned but should stay out of the runtime graph until a host
|
|||||||
| `ui.player-panels` | exclusive-owner | safe | Player panel contributions, mount/unmount, visibility, ordering. | A panel host with layout and focus rules. |
|
| `ui.player-panels` | exclusive-owner | safe | Player panel contributions, mount/unmount, visibility, ordering. | A panel host with layout and focus rules. |
|
||||||
| `ui.player-overlays` | exclusive-owner | safe | Overlay contributions layered over player or highway surfaces. | Overlay placement and z-order rules that coexist with legacy overlays. |
|
| `ui.player-overlays` | exclusive-owner | safe | Overlay contributions layered over player or highway surfaces. | Overlay placement and z-order rules that coexist with legacy overlays. |
|
||||||
| `plugins` | exclusive-owner | privileged | Plugin enable/disable/install/update workflows. | Visible user confirmation, rollback, and disabled-handler enforcement. |
|
| `plugins` | exclusive-owner | privileged | Plugin enable/disable/install/update workflows. | Visible user confirmation, rollback, and disabled-handler enforcement. |
|
||||||
| `jobs` | multi-provider | privileged | Long-running jobs, cancellation, status, failures. | Scheduling limits, cancellation semantics, and user-visible failures. |
|
|
||||||
| `midi-control` | multi-provider | sensitive | MIDI device providers and control mappings. | Device consent and redacted diagnostics. |
|
| `midi-control` | multi-provider | sensitive | MIDI device providers and control mappings. | Device consent and redacted diagnostics. |
|
||||||
| `audio-input` | multi-provider | sensitive | Audio input device providers, source selection, open/close lifecycle, shared sessions, and redacted failure diagnostics. | Promoted by the audio graph/session slice and implemented by the audio-input control-plane slice. |
|
| `audio-input` | multi-provider | sensitive | Audio input device providers, source selection, open/close lifecycle, shared sessions, and redacted failure diagnostics. | Promoted by the audio graph/session slice and implemented by the audio-input control-plane slice. |
|
||||||
| `tempo-clock` | multi-provider | safe | Tempo/clock provider registration and consumers. | A concrete tempo source and consumer workflow. |
|
| `tempo-clock` | multi-provider | safe | Tempo/clock provider registration and consumers. | A concrete tempo source and consumer workflow. |
|
||||||
|
|||||||
@@ -15,6 +15,8 @@ Core domains also have a review scope. **Active contract** domains are wired to
|
|||||||
| stems | coordinator plus plugin provider | safe | inspect, mute, restore | stem.get-state, stem.apply-automation, stem.restore-automation | Core coordinates claims/overrides; the active Stems provider owns actual stem state/playback. |
|
| stems | coordinator plus plugin provider | safe | inspect, mute, restore | stem.get-state, stem.apply-automation, stem.restore-automation | Core coordinates claims/overrides; the active Stems provider owns actual stem state/playback. |
|
||||||
| playback | exclusive-owner | safe | inspect, start, pause, resume, stop, seek, set-loop, clear-loop, register-requester, register-observer | none | Core owns the transport control plane while `app.js` keeps raw media handles private. Fresh audible starts require explicit user action. Diagnostics expose pseudonymous targets, sanitized route/timing/loop state, requester/observer summaries, bridge hits, bounded recent outcomes, and no audio elements, native handles, decoded buffers, samples, waveforms, or recordings. |
|
| playback | exclusive-owner | safe | inspect, start, pause, resume, stop, seek, set-loop, clear-loop, register-requester, register-observer | none | Core owns the transport control plane while `app.js` keeps raw media handles private. Fresh audible starts require explicit user action. Diagnostics expose pseudonymous targets, sanitized route/timing/loop state, requester/observer summaries, bridge hits, bounded recent outcomes, and no audio elements, native handles, decoded buffers, samples, waveforms, or recordings. |
|
||||||
| progression | exclusive-owner | safe | inspect, record-event, list-shop, buy-item, equip-item | none | Core owns mastery rank, the challenge/quest engine, the Decibels wallet, and the cosmetics shop (spec 010). `record-event` accepts whitelisted types only (`minigame_run`); `song_completed` is server-derived in `/api/stats` and denied here. `buy-item`/`equip-item` require explicit user action. Decibels are play-earned only — no real-money path exists or may be added. Diagnostics (`slopsmith.progression.diag.v1`) carry content warnings, rank/level/quest counts, and wallet totals; no song filenames or display names. |
|
| progression | exclusive-owner | safe | inspect, record-event, list-shop, buy-item, equip-item | none | Core owns mastery rank, the challenge/quest engine, the Decibels wallet, and the cosmetics shop (spec 010). `record-event` accepts whitelisted types only (`minigame_run`); `song_completed` is server-derived in `/api/stats` and denied here. `buy-item`/`equip-item` require explicit user action. Decibels are play-earned only — no real-money path exists or may be added. Diagnostics (`slopsmith.progression.diag.v1`) carry content warnings, rank/level/quest counts, and wallet totals; no song filenames or display names. |
|
||||||
|
|
||||||
|
| jobs | provider-coordinator | privileged | register-provider, unregister-provider, list-providers, enqueue, list, inspect, cancel, pause, resume, retry, record-bridge-hit | job.enqueue, job.status, job.cancel, job.pause, job.resume, job.retry, job.recover | Core coordinates provider selection, capacity scheduling, lifecycle state, recovery references, bridge hits, and diagnostics. Fresh privileged enqueue/retry requires explicit user approval or a matching approved-continuation scope. List/inspect are prompt-free and side-effect-free. Diagnostics redact paths, filenames, URLs, tokens, command lines, media/artifacts, recordings, handles, and provider-private payloads. |
|
||||||
| audio-effects | provider-coordinator | sensitive | inspect, list-providers, register-provider, unregister-provider, select-chain, resolve-plan, inspect-route, bypass, restore, fallback, activate-segment, set-stage-bypass, set-stage-parameter, record-bridge-hit | chain.resolve, chain.inspect, segment.activate, stage.set-bypass, stage.set-parameter, route.bypass, route.restore | Core owns provider selection, route state, chain-plan schema validation, fallback accounting, and diagnostics. Providers propose opaque NAM/IR/VST/utility chain plans; trusted desktop/native code validates and loads processors. Chain selection and route bypass/restore require explicit user action or restored selection. Diagnostics omit raw paths, filenames, URLs, model/IR names, native preset JSON, VST state blobs, handles, callbacks, DOM nodes, audio buffers, samples, and waveforms. |
|
| audio-effects | provider-coordinator | sensitive | inspect, list-providers, register-provider, unregister-provider, select-chain, resolve-plan, inspect-route, bypass, restore, fallback, activate-segment, set-stage-bypass, set-stage-parameter, record-bridge-hit | chain.resolve, chain.inspect, segment.activate, stage.set-bypass, stage.set-parameter, route.bypass, route.restore | Core owns provider selection, route state, chain-plan schema validation, fallback accounting, and diagnostics. Providers propose opaque NAM/IR/VST/utility chain plans; trusted desktop/native code validates and loads processors. Chain selection and route bypass/restore require explicit user action or restored selection. Diagnostics omit raw paths, filenames, URLs, model/IR names, native preset JSON, VST state blobs, handles, callbacks, DOM nodes, audio buffers, samples, and waveforms. |
|
||||||
|
|
||||||
| visualization | provider-coordinator | safe | inspect, list-providers, select-renderer, clear-renderer | renderer.create, renderer.destroy | Highway renderer provider registry, picker-delegated selection, auto-match attribution, and failure fallback. `renderer.create` maps to the legacy `window.slopsmithViz_*` factory `init(canvas, ctx)` call; `renderer.destroy` maps to the factory `destroy()` teardown. Legacy `type: "visualization"` manifests and `window.slopsmithViz_*` globals are accounted compatibility shims. Diagnostics carry provider ids/labels, selection source, last auto-match outcome, and last failure — no song filenames, titles, or arrangement names. |
|
| visualization | provider-coordinator | safe | inspect, list-providers, select-renderer, clear-renderer | renderer.create, renderer.destroy | Highway renderer provider registry, picker-delegated selection, auto-match attribution, and failure fallback. `renderer.create` maps to the legacy `window.slopsmithViz_*` factory `init(canvas, ctx)` call; `renderer.destroy` maps to the factory `destroy()` teardown. Legacy `type: "visualization"` manifests and `window.slopsmithViz_*` globals are accounted compatibility shims. Diagnostics carry provider ids/labels, selection source, last auto-match outcome, and last failure — no song filenames, titles, or arrangement names. |
|
||||||
@@ -37,7 +39,6 @@ These domains are expected future capability contracts, not current runtime grap
|
|||||||
| ui.player-panels | exclusive-owner | safe | register-contribution, mount, unmount, set-visible, reorder-by-policy, inspect | Needs a first-party panel host and layout policy. |
|
| ui.player-panels | exclusive-owner | safe | register-contribution, mount, unmount, set-visible, reorder-by-policy, inspect | Needs a first-party panel host and layout policy. |
|
||||||
| ui.player-overlays | exclusive-owner | safe | register-contribution, mount, unmount, set-visible, reorder-by-policy, inspect | Needs overlay placement rules that coexist with legacy highway overlays. |
|
| ui.player-overlays | exclusive-owner | safe | register-contribution, mount, unmount, set-visible, reorder-by-policy, inspect | Needs overlay placement rules that coexist with legacy highway overlays. |
|
||||||
| plugins | exclusive-owner | privileged | enable, disable, install-missing, update, inspect | Needs explicit user confirmation for writes/install/update. |
|
| plugins | exclusive-owner | privileged | enable, disable, install-missing, update, inspect | Needs explicit user confirmation for writes/install/update. |
|
||||||
| jobs | multi-provider | privileged | register, inspect, cancel | Needs scheduling limits, cancellation semantics, and user-visible failures. |
|
|
||||||
| midi-control | multi-provider | sensitive | register, inspect | Needs device consent and redacted diagnostics. |
|
| midi-control | multi-provider | sensitive | register, inspect | Needs device consent and redacted diagnostics. |
|
||||||
| tempo-clock | multi-provider | safe | register, inspect | Needs a concrete provider and consumer workflow. |
|
| tempo-clock | multi-provider | safe | register, inspect | Needs a concrete provider and consumer workflow. |
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,6 @@
|
|||||||
'ui.player-panels',
|
'ui.player-panels',
|
||||||
'ui.player-overlays',
|
'ui.player-overlays',
|
||||||
'plugins',
|
'plugins',
|
||||||
'jobs',
|
|
||||||
'midi-control',
|
'midi-control',
|
||||||
'tempo-clock',
|
'tempo-clock',
|
||||||
]);
|
]);
|
||||||
@@ -19,8 +18,8 @@
|
|||||||
Object.freeze({
|
Object.freeze({
|
||||||
id: 'app-library',
|
id: 'app-library',
|
||||||
label: 'Application and Library',
|
label: 'Application and Library',
|
||||||
summary: 'Navigation, plugin screens, settings, library sources, and tunings.',
|
summary: 'Navigation, plugin screens, settings, library/tuning sources, and long-running jobs.',
|
||||||
domains: Object.freeze(['ui.navigation', 'ui.plugin-screens', 'settings', 'library', 'tuning']),
|
domains: Object.freeze(['ui.navigation', 'ui.plugin-screens', 'settings', 'library', 'tuning', 'jobs']),
|
||||||
}),
|
}),
|
||||||
Object.freeze({
|
Object.freeze({
|
||||||
id: 'player-audio',
|
id: 'player-audio',
|
||||||
@@ -1474,6 +1473,54 @@
|
|||||||
catch (_) { return null; }
|
catch (_) { return null; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function jobsSnapshot() {
|
||||||
|
const api = window.slopsmith && window.slopsmith.jobs;
|
||||||
|
if (!api || typeof api.snapshot !== 'function') return null;
|
||||||
|
try { return api.snapshot({ exportMode: 'local-inspector' }); }
|
||||||
|
catch (_) { return null; }
|
||||||
|
}
|
||||||
|
|
||||||
|
function jobsSupportPanel(jobsData) {
|
||||||
|
if (!jobsData || !jobsData.jobs) return '';
|
||||||
|
const providers = Array.isArray(jobsData.providers) ? jobsData.providers : [];
|
||||||
|
const selectedProviders = Array.isArray(jobsData.selectedProviders) ? jobsData.selectedProviders : [];
|
||||||
|
const active = Array.isArray(jobsData.jobs.active) ? jobsData.jobs.active : [];
|
||||||
|
const queued = Array.isArray(jobsData.jobs.queued) ? jobsData.jobs.queued : [];
|
||||||
|
const paused = Array.isArray(jobsData.jobs.paused) ? jobsData.jobs.paused : [];
|
||||||
|
const recentTerminal = Array.isArray(jobsData.jobs.recentTerminal) ? jobsData.jobs.recentTerminal : [];
|
||||||
|
const outcomes = Array.isArray(jobsData.outcomes) ? jobsData.outcomes.slice(-6) : [];
|
||||||
|
const bridgeHits = Array.isArray(jobsData.bridgeHits) ? jobsData.bridgeHits : [];
|
||||||
|
const unavailableProviders = providers.filter(provider => provider.availability && provider.availability !== 'available').length;
|
||||||
|
const terminalFailures = recentTerminal.filter(job => job && job.terminalOutcome && ['failed', 'timeout', 'provider-unavailable'].includes(job.terminalOutcome.status)).length;
|
||||||
|
return `<section class="mb-4 rounded-lg border border-gray-800 bg-dark-900/40 p-4" data-jobs-support>
|
||||||
|
<div class="flex flex-wrap items-center justify-between gap-3">
|
||||||
|
<div>
|
||||||
|
<h3 class="text-sm font-semibold text-white">Jobs</h3>
|
||||||
|
<p class="mt-1 text-xs text-gray-500">Provider selection, active work, scheduling, terminal outcomes, and bridge hits from the jobs host.</p>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-wrap gap-2">
|
||||||
|
${pill(`${providers.length} provider${providers.length === 1 ? '' : 's'}`, providers.length ? 'info' : 'muted')}
|
||||||
|
${pill(`${active.length} active`, active.length ? 'used' : 'muted')}
|
||||||
|
${pill(`${queued.length} queued`, queued.length ? 'warning' : 'muted')}
|
||||||
|
${pill(`${paused.length} paused`, paused.length ? 'warning' : 'muted')}
|
||||||
|
${pill(`${recentTerminal.length} recent terminal`, recentTerminal.length ? (terminalFailures ? 'conflict' : 'clean') : 'muted')}
|
||||||
|
${pill(`${bridgeHits.length} bridge hit${bridgeHits.length === 1 ? '' : 's'}`, bridgeHits.length ? 'used' : 'muted')}
|
||||||
|
${pill(`${unavailableProviders} unavailable provider${unavailableProviders === 1 ? '' : 's'}`, unavailableProviders ? 'warning' : 'muted')}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="mt-3 grid gap-2 text-xs text-gray-400 md:grid-cols-2">
|
||||||
|
<div data-jobs-providers>Providers: ${providers.map(provider => `${text(provider.label || provider.providerId)}:${text(provider.availability || 'unknown')}:${text((provider.jobTypes || []).join('+') || 'none')}`).join(', ') || 'none'}</div>
|
||||||
|
<div data-jobs-selected>Selected: ${selectedProviders.map(selection => `${text(selection.jobType)}:${text(selection.providerId)}:${text(selection.source || 'stored')}`).join(', ') || 'none'}</div>
|
||||||
|
<div data-jobs-active>Active: ${active.map(job => `${text(job.safeLabel || job.jobId)}:${text(job.providerId)}:${text(job.progress && job.progress.percent != null ? `${job.progress.percent}%` : job.progress && job.progress.step || job.state)}`).join(', ') || 'none'}</div>
|
||||||
|
<div data-jobs-queued>Queued: ${queued.map(job => `${text(job.safeLabel || job.jobId)}:${text(job.providerId)}:${text(job.priority || 'normal')}`).join(', ') || 'none'}</div>
|
||||||
|
<div data-jobs-paused>Paused: ${paused.map(job => `${text(job.safeLabel || job.jobId)}:${text(job.providerId)}`).join(', ') || 'none'}</div>
|
||||||
|
<div data-jobs-terminal>Terminal: ${recentTerminal.map(job => `${text(job.safeLabel || job.jobId)}:${text(job.terminalOutcome && job.terminalOutcome.status || job.state)}${job.terminalOutcome && job.terminalOutcome.category ? `:${text(job.terminalOutcome.category)}` : ''}`).join(', ') || 'none'}</div>
|
||||||
|
<div data-jobs-bridges>Bridges: ${bridgeHits.map(hit => `${text(hit.bridgeId || hit.legacySurface)}:${text(hit.operation || 'unknown')}${hit.diagnosticsOnly ? ':diagnostics-only' : ''}`).join(', ') || 'none'}</div>
|
||||||
|
<div data-jobs-outcomes>Outcomes: ${outcomes.map(outcome => `${text(outcome.operation)}:${text(outcome.status || outcome.outcome)}`).join(', ') || 'none'}</div>
|
||||||
|
</div>
|
||||||
|
</section>`;
|
||||||
|
}
|
||||||
|
|
||||||
function playbackSupportPanel(playbackData) {
|
function playbackSupportPanel(playbackData) {
|
||||||
if (!playbackData || !playbackData.state) return '';
|
if (!playbackData || !playbackData.state) return '';
|
||||||
const state = playbackData.state || {};
|
const state = playbackData.state || {};
|
||||||
@@ -1642,9 +1689,10 @@
|
|||||||
const compatibilityShims = Array.isArray(data.compatibilityShims) ? data.compatibilityShims : [];
|
const compatibilityShims = Array.isArray(data.compatibilityShims) ? data.compatibilityShims : [];
|
||||||
if (summary) summary.innerHTML = summaryDashboard(data, pipelines, compatibilityShims);
|
if (summary) summary.innerHTML = summaryDashboard(data, pipelines, compatibilityShims);
|
||||||
destroyActiveGraphs();
|
destroyActiveGraphs();
|
||||||
|
const jobsPanel = jobsSupportPanel(jobsSnapshot());
|
||||||
const playbackPanel = playbackSupportPanel(playbackSnapshot());
|
const playbackPanel = playbackSupportPanel(playbackSnapshot());
|
||||||
const audioPanel = audioDomainSupportPanel(audioSessionSnapshot());
|
const audioPanel = audioDomainSupportPanel(audioSessionSnapshot());
|
||||||
content.innerHTML = playbackPanel + audioPanel + (selected
|
content.innerHTML = jobsPanel + playbackPanel + audioPanel + (selected
|
||||||
? visible.map(pipeline => domainGraphView(pipeline, shimsByCapability.get(pipeline.name) || [], expectedShimsByCapability.get(pipeline.name) || [])).join('')
|
? visible.map(pipeline => domainGraphView(pipeline, shimsByCapability.get(pipeline.name) || [], expectedShimsByCapability.get(pipeline.name) || [])).join('')
|
||||||
: groupedPipelines(visible).map(entry => pipelineGroupSection(entry, shimsByCapability, expectedShimsByCapability, { defaultExpanded: false })).join(''))
|
: groupedPipelines(visible).map(entry => pipelineGroupSection(entry, shimsByCapability, expectedShimsByCapability, { defaultExpanded: false })).join(''))
|
||||||
|| '<div class="text-gray-500 text-sm">No capability domains registered.</div>';
|
|| '<div class="text-gray-500 text-sm">No capability domains registered.</div>';
|
||||||
|
|||||||
+72
-72
@@ -17,11 +17,11 @@
|
|||||||
|
|
||||||
**Purpose**: Create the jobs capability host entry points and test harness scaffolding used by all stories.
|
**Purpose**: Create the jobs capability host entry points and test harness scaffolding used by all stories.
|
||||||
|
|
||||||
- [ ] T001 Create jobs capability host shell with IIFE, event-bus access, diagnostics contribution placeholder, and public namespace in static/capabilities/jobs.js
|
- [X] T001 Create jobs capability host shell with IIFE, event-bus access, diagnostics contribution placeholder, and public namespace in static/capabilities/jobs.js
|
||||||
- [ ] T002 Load the jobs capability host after the capability runtime in static/index.html
|
- [X] T002 Load the jobs capability host after the capability runtime in static/index.html
|
||||||
- [ ] T003 [P] Create jobs Node VM test harness with helpers for loading capabilities, dispatching jobs commands, capturing events, and reading diagnostics in tests/js/jobs_test_harness.js
|
- [X] T003 [P] Create jobs Node VM test harness with helpers for loading capabilities, dispatching jobs commands, capturing events, and reading diagnostics in tests/js/jobs_test_harness.js
|
||||||
- [ ] T004 [P] Add jobs test file placeholders importing the harness in tests/js/jobs_domain.test.js, tests/js/jobs_scheduling.test.js, tests/js/jobs_diagnostics.test.js, and tests/js/jobs_compat.test.js
|
- [X] T004 [P] Add jobs test file placeholders importing the harness in tests/js/jobs_domain.test.js, tests/js/jobs_scheduling.test.js, tests/js/jobs_diagnostics.test.js, and tests/js/jobs_compat.test.js
|
||||||
- [ ] T005 [P] Add jobs inspector render fixture placeholders in tests/js/capability_inspector_render.test.js
|
- [X] T005 [P] Add jobs inspector render fixture placeholders in tests/js/capability_inspector_render.test.js
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -31,15 +31,15 @@
|
|||||||
|
|
||||||
**CRITICAL**: No user story work can begin until this phase is complete.
|
**CRITICAL**: No user story work can begin until this phase is complete.
|
||||||
|
|
||||||
- [ ] T006 Promote `jobs` from reserved future domain to active privileged provider-coordinator review metadata in static/capabilities.js
|
- [X] T006 Promote `jobs` from reserved future domain to active privileged provider-coordinator review metadata in static/capabilities.js
|
||||||
- [ ] T007 Add or verify the canonical jobs outcome inventory from FR-005 in capability outcome normalization in static/capabilities.js
|
- [X] T007 Add or verify the canonical jobs outcome inventory from FR-005 in capability outcome normalization in static/capabilities.js
|
||||||
- [ ] T008 Implement constants for job states, outcomes, actions, priorities, limits, bridge ids, and diagnostics schema in static/capabilities/jobs.js
|
- [X] T008 Implement constants for job states, outcomes, actions, priorities, limits, bridge ids, and diagnostics schema in static/capabilities/jobs.js
|
||||||
- [ ] T009 Implement redaction and safe-value helpers for labels, reasons, target refs, logs, command-like text, URLs, local paths, and provider payloads in static/capabilities/jobs.js
|
- [X] T009 Implement redaction and safe-value helpers for labels, reasons, target refs, logs, command-like text, URLs, local paths, and provider payloads in static/capabilities/jobs.js
|
||||||
- [ ] T010 Implement provider, selected-provider, job, attempt, progress, outcome, history, and bridge in-memory stores with resettable test hooks in static/capabilities/jobs.js
|
- [X] T010 Implement provider, selected-provider, job, attempt, progress, outcome, history, and bridge in-memory stores with resettable test hooks in static/capabilities/jobs.js
|
||||||
- [ ] T011 Implement diagnostics snapshot builder with active/queued/paused/recent terminal grouping and 64 KB budget trimming in static/capabilities/jobs.js
|
- [X] T011 Implement diagnostics snapshot builder with active/queued/paused/recent terminal grouping and 64 KB budget trimming in static/capabilities/jobs.js
|
||||||
- [ ] T012 Implement jobs capability owner registration with command/event declarations and diagnostics contribution in static/capabilities/jobs.js
|
- [X] T012 Implement jobs capability owner registration with command/event declarations and diagnostics contribution in static/capabilities/jobs.js
|
||||||
- [ ] T013 Add foundational harness assertions for active jobs owner registration, diagnostics schema, reset hooks, and no raw-provider-payload exposure in tests/js/jobs_domain.test.js
|
- [X] T013 Add foundational harness assertions for active jobs owner registration, diagnostics schema, reset hooks, and no raw-provider-payload exposure in tests/js/jobs_domain.test.js
|
||||||
- [ ] T014 [P] Add static idempotence expectations for active jobs domain and new outcomes in tests/test_plugin_runtime_idempotence.py
|
- [X] T014 [P] Add static idempotence expectations for active jobs domain and new outcomes in tests/test_plugin_runtime_idempotence.py
|
||||||
|
|
||||||
**Checkpoint**: Jobs domain loads, registers as active privileged provider-coordinator, and can produce an empty redaction-safe diagnostics snapshot.
|
**Checkpoint**: Jobs domain loads, registers as active privileged provider-coordinator, and can produce an empty redaction-safe diagnostics snapshot.
|
||||||
|
|
||||||
@@ -53,21 +53,21 @@
|
|||||||
|
|
||||||
### Tests for User Story 1
|
### Tests for User Story 1
|
||||||
|
|
||||||
- [ ] T015 [US1] Add provider registration, re-registration idempotence, unavailable/degraded provider, and incompatible provider tests in tests/js/jobs_domain.test.js
|
- [X] T015 [US1] Add provider registration, re-registration idempotence, unavailable/degraded provider, and incompatible provider tests in tests/js/jobs_domain.test.js
|
||||||
- [ ] T016 [US1] Add user-approved enqueue, privileged enqueue without approval returning denied/user-action-required before provider work starts, side-effect-free list/inspect, queued/running state, no-owner, unavailable, and validation-failed tests in tests/js/jobs_domain.test.js
|
- [X] T016 [US1] Add user-approved enqueue, privileged enqueue without approval returning denied/user-action-required before provider work starts, side-effect-free list/inspect, queued/running state, no-owner, unavailable, and validation-failed tests in tests/js/jobs_domain.test.js
|
||||||
- [ ] T017 [US1] Add determinate progress, indeterminate progress, stale progress after terminal state, completion, and safe result summary tests in tests/js/jobs_domain.test.js
|
- [X] T017 [US1] Add determinate progress, indeterminate progress, stale progress after terminal state, completion, and safe result summary tests in tests/js/jobs_domain.test.js
|
||||||
- [ ] T018 [P] [US1] Add Capability Inspector jobs provider/progress/current-job rendering tests in tests/js/capability_inspector_render.test.js
|
- [X] T018 [P] [US1] Add Capability Inspector jobs provider/progress/current-job rendering tests in tests/js/capability_inspector_render.test.js
|
||||||
|
|
||||||
### Implementation for User Story 1
|
### Implementation for User Story 1
|
||||||
|
|
||||||
- [ ] T019 [US1] Implement `register-provider`, `unregister-provider`, `list-providers`, provider validation, and idempotent provider updates in static/capabilities/jobs.js
|
- [X] T019 [US1] Implement `register-provider`, `unregister-provider`, `list-providers`, provider validation, and idempotent provider updates in static/capabilities/jobs.js
|
||||||
- [ ] T020 [US1] Implement `enqueue` validation for one compatible provider, explicit user-action or approved-continuation authorization before provider dispatch, provider availability, provider operation dispatch, job creation, queued/running state, and denied/user-action-required/no-owner/no-handler/unavailable outcomes in static/capabilities/jobs.js
|
- [X] T020 [US1] Implement `enqueue` validation for one compatible provider, explicit user-action or approved-continuation authorization before provider dispatch, provider availability, provider operation dispatch, job creation, queued/running state, and denied/user-action-required/no-owner/no-handler/unavailable outcomes in static/capabilities/jobs.js
|
||||||
- [ ] T021 [US1] Implement side-effect-free `list` and `inspect` command summaries for providers, jobs, progress, attempts, actions, timestamps, and safe reasons without invoking provider work callbacks in static/capabilities/jobs.js
|
- [X] T021 [US1] Implement side-effect-free `list` and `inspect` command summaries for providers, jobs, progress, attempts, actions, timestamps, and safe reasons without invoking provider work callbacks in static/capabilities/jobs.js
|
||||||
- [ ] T022 [US1] Implement provider progress and log update ingestion with determinate, indeterminate, step-only, decreasing-progress, and stale-after-terminal handling in static/capabilities/jobs.js
|
- [X] T022 [US1] Implement provider progress and log update ingestion with determinate, indeterminate, step-only, decreasing-progress, and stale-after-terminal handling in static/capabilities/jobs.js
|
||||||
- [ ] T023 [US1] Implement provider completion and failure result normalization with terminal outcomes, retryability, result summaries, and active-state cleanup in static/capabilities/jobs.js
|
- [X] T023 [US1] Implement provider completion and failure result normalization with terminal outcomes, retryability, result summaries, and active-state cleanup in static/capabilities/jobs.js
|
||||||
- [ ] T024 [US1] Emit `jobs:provider-registered`, `jobs:queued`, `jobs:started`, `jobs:progress`, `jobs:log`, `jobs:completed`, and `jobs:failed` lifecycle events in static/capabilities/jobs.js
|
- [X] T024 [US1] Emit `jobs:provider-registered`, `jobs:queued`, `jobs:started`, `jobs:progress`, `jobs:log`, `jobs:completed`, and `jobs:failed` lifecycle events in static/capabilities/jobs.js
|
||||||
- [ ] T025 [US1] Render jobs provider cards, active/queued jobs, progress, action availability, and recent outcomes in plugins/capability_inspector/screen.js
|
- [X] T025 [US1] Render jobs provider cards, active/queued jobs, progress, action availability, and recent outcomes in plugins/capability_inspector/screen.js
|
||||||
- [ ] T026 [US1] Load and smoke-check jobs host script ordering with index integration in static/index.html
|
- [X] T026 [US1] Load and smoke-check jobs host script ordering with index integration in static/index.html
|
||||||
|
|
||||||
**Checkpoint**: User Story 1 is independently functional; a single provider can enqueue, progress, complete/fail, and appear in diagnostics and inspector.
|
**Checkpoint**: User Story 1 is independently functional; a single provider can enqueue, progress, complete/fail, and appear in diagnostics and inspector.
|
||||||
|
|
||||||
@@ -81,20 +81,20 @@
|
|||||||
|
|
||||||
### Tests for User Story 2
|
### Tests for User Story 2
|
||||||
|
|
||||||
- [ ] T027 [US2] Add queued cancel, running cancellation-requested, provider terminal after cancellation, late cancel stale, and unsupported cancel tests in tests/js/jobs_domain.test.js
|
- [X] T027 [US2] Add queued cancel, running cancellation-requested, provider terminal after cancellation, late cancel stale, and unsupported cancel tests in tests/js/jobs_domain.test.js
|
||||||
- [ ] T028 [US2] Add pause/resume supported, unsupported pause/resume, terminal pause/resume stale, and same-job-identity tests in tests/js/jobs_domain.test.js
|
- [X] T028 [US2] Add pause/resume supported, unsupported pause/resume, terminal pause/resume stale, and same-job-identity tests in tests/js/jobs_domain.test.js
|
||||||
- [ ] T029 [US2] Add retryable failed/cancelled job, linked attempt, approved-continuation scope, widened-scope denial, and concurrent retry tests in tests/js/jobs_domain.test.js
|
- [X] T029 [US2] Add retryable failed/cancelled job, linked attempt, approved-continuation scope, widened-scope denial, and concurrent retry tests in tests/js/jobs_domain.test.js
|
||||||
- [ ] T030 [P] [US2] Add inspector tests for cancellation-requested, paused, retryable terminal jobs, and linked attempts in tests/js/capability_inspector_render.test.js
|
- [X] T030 [P] [US2] Add inspector tests for cancellation-requested, paused, retryable terminal jobs, and linked attempts in tests/js/capability_inspector_render.test.js
|
||||||
|
|
||||||
### Implementation for User Story 2
|
### Implementation for User Story 2
|
||||||
|
|
||||||
- [ ] T031 [US2] Implement `cancel` command for queued immediate cancellation, running cancellation-requested, unsupported-operation, stale terminal, and provider failure outcomes in static/capabilities/jobs.js
|
- [X] T031 [US2] Implement `cancel` command for queued immediate cancellation, running cancellation-requested, unsupported-operation, stale terminal, and provider failure outcomes in static/capabilities/jobs.js
|
||||||
- [ ] T032 [US2] Implement `pause` command with provider action checks, paused state, unsupported-operation, stale terminal, and safe reason outcomes in static/capabilities/jobs.js
|
- [X] T032 [US2] Implement `pause` command with provider action checks, paused state, unsupported-operation, stale terminal, and safe reason outcomes in static/capabilities/jobs.js
|
||||||
- [ ] T033 [US2] Implement `resume` command returning paused jobs to queued/running state with the same job identity in static/capabilities/jobs.js
|
- [X] T033 [US2] Implement `resume` command returning paused jobs to queued/running state with the same job identity in static/capabilities/jobs.js
|
||||||
- [ ] T034 [US2] Implement `retry` command with terminal-state checks, retryability, one active retry attempt, linked attempts, approved-continuation scope, and retry-started outcomes in static/capabilities/jobs.js
|
- [X] T034 [US2] Implement `retry` command with terminal-state checks, retryability, one active retry attempt, linked attempts, approved-continuation scope, and retry-started outcomes in static/capabilities/jobs.js
|
||||||
- [ ] T035 [US2] Implement approval scope matching for provider, job type, target, requester, and inputs in static/capabilities/jobs.js
|
- [X] T035 [US2] Implement approval scope matching for provider, job type, target, requester, and inputs in static/capabilities/jobs.js
|
||||||
- [ ] T036 [US2] Emit `jobs:cancellation-requested`, `jobs:cancelled`, `jobs:paused`, `jobs:resumed`, and `jobs:retried` events in static/capabilities/jobs.js
|
- [X] T036 [US2] Emit `jobs:cancellation-requested`, `jobs:cancelled`, `jobs:paused`, `jobs:resumed`, and `jobs:retried` events in static/capabilities/jobs.js
|
||||||
- [ ] T037 [US2] Update Capability Inspector jobs rendering for cancellation state, pause/resume actions, retry action, and attempt history in plugins/capability_inspector/screen.js
|
- [X] T037 [US2] Update Capability Inspector jobs rendering for cancellation state, pause/resume actions, retry action, and attempt history in plugins/capability_inspector/screen.js
|
||||||
|
|
||||||
**Checkpoint**: User Story 2 is independently functional; all job control actions produce explicit outcomes and keep job state truthful.
|
**Checkpoint**: User Story 2 is independently functional; all job control actions produce explicit outcomes and keep job state truthful.
|
||||||
|
|
||||||
@@ -108,22 +108,22 @@
|
|||||||
|
|
||||||
### Tests for User Story 3
|
### Tests for User Story 3
|
||||||
|
|
||||||
- [ ] T038 [US3] Add exactly-one-provider auto-select, multiple-provider provider-selection-required, selected/default provider, unavailable selection, and explicit provider tests in tests/js/jobs_scheduling.test.js
|
- [X] T038 [US3] Add exactly-one-provider auto-select, multiple-provider provider-selection-required, selected/default provider, unavailable selection, and explicit provider tests in tests/js/jobs_scheduling.test.js
|
||||||
- [ ] T039 [US3] Add provider capacity, queued blocked reason, user-approved-before-background priority, FIFO within priority, and no overrun tests in tests/js/jobs_scheduling.test.js
|
- [X] T039 [US3] Add provider capacity, queued blocked reason, user-approved-before-background priority, FIFO within priority, and no overrun tests in tests/js/jobs_scheduling.test.js
|
||||||
- [ ] T040 [US3] Add provider disappearance, provider-unavailable/orphaned state, provider rehydration no duplicates, and incompatible version tests in tests/js/jobs_scheduling.test.js
|
- [X] T040 [US3] Add provider disappearance, provider-unavailable/orphaned state, provider rehydration no duplicates, and incompatible version tests in tests/js/jobs_scheduling.test.js
|
||||||
- [ ] T041 [US3] Add reload recovery tests for persisted recoverable queued/running/paused job references, non-recoverable orphan/provider-unavailable jobs, stale persisted reference cleanup, and terminal preservation in tests/js/jobs_scheduling.test.js
|
- [X] T041 [US3] Add reload recovery tests for persisted recoverable queued/running/paused job references, non-recoverable orphan/provider-unavailable jobs, stale persisted reference cleanup, and terminal preservation in tests/js/jobs_scheduling.test.js
|
||||||
- [ ] T042 [P] [US3] Add selected/default provider persistence fallback tests in tests/js/jobs_scheduling.test.js
|
- [X] T042 [P] [US3] Add selected/default provider persistence fallback tests in tests/js/jobs_scheduling.test.js
|
||||||
|
|
||||||
### Implementation for User Story 3
|
### Implementation for User Story 3
|
||||||
|
|
||||||
- [ ] T043 [US3] Implement selected/default provider state, localStorage persistence with in-memory fallback, and provider-selection-required logic in static/capabilities/jobs.js
|
- [X] T043 [US3] Implement selected/default provider state, localStorage persistence with in-memory fallback, and provider-selection-required logic in static/capabilities/jobs.js
|
||||||
- [ ] T044 [US3] Implement scheduler capacity checks, queued blocked reasons, user-approved-interactive priority before background-maintenance, and FIFO ordering within priority in static/capabilities/jobs.js
|
- [X] T044 [US3] Implement scheduler capacity checks, queued blocked reasons, user-approved-interactive priority before background-maintenance, and FIFO ordering within priority in static/capabilities/jobs.js
|
||||||
- [ ] T045 [US3] Implement start-next scheduling after enqueue, completion, cancellation, provider availability change, and resume in static/capabilities/jobs.js
|
- [X] T045 [US3] Implement start-next scheduling after enqueue, completion, cancellation, provider availability change, and resume in static/capabilities/jobs.js
|
||||||
- [ ] T046 [US3] Implement provider disappearance and incompatible-provider handling that marks affected jobs provider-unavailable or orphaned with safe reasons in static/capabilities/jobs.js
|
- [X] T046 [US3] Implement provider disappearance and incompatible-provider handling that marks affected jobs provider-unavailable or orphaned with safe reasons in static/capabilities/jobs.js
|
||||||
- [ ] T047 [US3] Implement provider rehydration and recoverable job reconciliation without duplicating providers, active jobs, terminal jobs, or attempts in static/capabilities/jobs.js
|
- [X] T047 [US3] Implement provider rehydration and recoverable job reconciliation without duplicating providers, active jobs, terminal jobs, or attempts in static/capabilities/jobs.js
|
||||||
- [ ] T048 [US3] Implement reload recovery hooks that persist only redaction-safe provider-declared recoverable queued/running/paused job references, restore them on reload, clean stale references, and mark non-recoverable jobs orphan/provider-unavailable in static/capabilities/jobs.js
|
- [X] T048 [US3] Implement reload recovery hooks that persist only redaction-safe provider-declared recoverable queued/running/paused job references, restore them on reload, clean stale references, and mark non-recoverable jobs orphan/provider-unavailable in static/capabilities/jobs.js
|
||||||
- [ ] T049 [US3] Emit `jobs:provider-unavailable` and `jobs:orphaned` events and scheduling-related queued/started events in static/capabilities/jobs.js
|
- [X] T049 [US3] Emit `jobs:provider-unavailable` and `jobs:orphaned` events and scheduling-related queued/started events in static/capabilities/jobs.js
|
||||||
- [ ] T050 [US3] Update Capability Inspector jobs rendering for provider selection, capacity/current load, queued blocked reason, priority, and recovery status in plugins/capability_inspector/screen.js
|
- [X] T050 [US3] Update Capability Inspector jobs rendering for provider selection, capacity/current load, queued blocked reason, priority, and recovery status in plugins/capability_inspector/screen.js
|
||||||
|
|
||||||
**Checkpoint**: User Story 3 is independently functional; providers can be selected, scheduled, recovered, and diagnosed without duplicate queues or over-capacity starts.
|
**Checkpoint**: User Story 3 is independently functional; providers can be selected, scheduled, recovered, and diagnosed without duplicate queues or over-capacity starts.
|
||||||
|
|
||||||
@@ -137,21 +137,21 @@
|
|||||||
|
|
||||||
### Tests for User Story 4
|
### Tests for User Story 4
|
||||||
|
|
||||||
- [ ] T051 [US4] Add diagnostics schema, active/queued/paused/recent terminal grouping, all-active retention, five-terminal retention, and 50-entry history cap tests in tests/js/jobs_diagnostics.test.js
|
- [X] T051 [US4] Add diagnostics schema, active/queued/paused/recent terminal grouping, all-active retention, five-terminal retention, and 50-entry history cap tests in tests/js/jobs_diagnostics.test.js
|
||||||
- [ ] T052 [US4] Add redaction tests for paths, raw filenames, URLs, tokens, command lines, environment values, raw artifacts, media buffers, recordings, native handles, subprocess handles, and provider-private payloads in tests/js/jobs_diagnostics.test.js
|
- [X] T052 [US4] Add redaction tests for paths, raw filenames, URLs, tokens, command lines, environment values, raw artifacts, media buffers, recordings, native handles, subprocess handles, and provider-private payloads in tests/js/jobs_diagnostics.test.js
|
||||||
- [ ] T053 [US4] Add failure category tests for invalid-input, permission-denied, provider-unavailable, unsupported-operation, timeout, cancellation, external-dependency, storage, provider-failure, and unknown in tests/js/jobs_diagnostics.test.js
|
- [X] T053 [US4] Add failure category tests for invalid-input, permission-denied, provider-unavailable, unsupported-operation, timeout, cancellation, external-dependency, storage, provider-failure, and unknown in tests/js/jobs_diagnostics.test.js
|
||||||
- [ ] T054 [US4] Add compatibility bridge hit, legacy duplicate suppression, legacy queue/status/backend-route operations, and diagnostics-only bridge tests in tests/js/jobs_compat.test.js
|
- [X] T054 [US4] Add compatibility bridge hit, legacy duplicate suppression, legacy queue/status/backend-route operations, and diagnostics-only bridge tests in tests/js/jobs_compat.test.js
|
||||||
- [ ] T055 [P] [US4] Add inspector diagnostics tests for terminal jobs, safe failure category, bridge hits, retention limits, and redacted log messages in tests/js/capability_inspector_render.test.js
|
- [X] T055 [P] [US4] Add inspector diagnostics tests for terminal jobs, safe failure category, bridge hits, retention limits, and redacted log messages in tests/js/capability_inspector_render.test.js
|
||||||
|
|
||||||
### Implementation for User Story 4
|
### Implementation for User Story 4
|
||||||
|
|
||||||
- [ ] T056 [US4] Implement diagnostics payload `slopsmith.jobs.diagnostics.v1` with providers, selectedProviders, jobs, outcomes, bridgeHits, limits, and notes in static/capabilities/jobs.js
|
- [X] T056 [US4] Implement diagnostics payload `slopsmith.jobs.diagnostics.v1` with providers, selectedProviders, jobs, outcomes, bridgeHits, limits, and notes in static/capabilities/jobs.js
|
||||||
- [ ] T057 [US4] Implement retention policy preserving all active jobs, at least five recent terminal jobs, and at most 50 progress/log entries per job within snapshot budget in static/capabilities/jobs.js
|
- [X] T057 [US4] Implement retention policy preserving all active jobs, at least five recent terminal jobs, and at most 50 progress/log entries per job within snapshot budget in static/capabilities/jobs.js
|
||||||
- [ ] T058 [US4] Implement failure category normalization and safe retryability summaries for invalid input, permission denied, provider unavailable, unsupported operation, timeout, cancellation, external dependency, storage, provider failure, and unknown in static/capabilities/jobs.js
|
- [X] T058 [US4] Implement failure category normalization and safe retryability summaries for invalid input, permission denied, provider unavailable, unsupported operation, timeout, cancellation, external dependency, storage, provider failure, and unknown in static/capabilities/jobs.js
|
||||||
- [ ] T059 [US4] Implement redaction/pseudonymization enforcement for exported diagnostics and local inspector snapshots in static/capabilities/jobs.js
|
- [X] T059 [US4] Implement redaction/pseudonymization enforcement for exported diagnostics and local inspector snapshots in static/capabilities/jobs.js
|
||||||
- [ ] T060 [US4] Implement `record-bridge-hit` command and bridge hit normalization for legacy plugin queue, status screen, backend route, progress poll, and update flow in static/capabilities/jobs.js
|
- [X] T060 [US4] Implement `record-bridge-hit` command and bridge hit normalization for legacy plugin queue, status screen, backend route, progress poll, and update flow in static/capabilities/jobs.js
|
||||||
- [ ] T061 [US4] Implement native-over-compatibility duplicate suppression for logical jobs shared by native providers and compatibility-backed legacy surfaces in static/capabilities/jobs.js
|
- [X] T061 [US4] Implement native-over-compatibility duplicate suppression for logical jobs shared by native providers and compatibility-backed legacy surfaces in static/capabilities/jobs.js
|
||||||
- [ ] T062 [US4] Update Capability Inspector jobs rendering for recent terminal jobs, failure categories, retention notes, bridge hits, and redacted history in plugins/capability_inspector/screen.js
|
- [X] T062 [US4] Update Capability Inspector jobs rendering for recent terminal jobs, failure categories, retention notes, bridge hits, and redacted history in plugins/capability_inspector/screen.js
|
||||||
|
|
||||||
**Checkpoint**: User Story 4 is independently functional; support surfaces explain failures and migration bridges without leaking privileged data.
|
**Checkpoint**: User Story 4 is independently functional; support surfaces explain failures and migration bridges without leaking privileged data.
|
||||||
|
|
||||||
@@ -161,16 +161,16 @@
|
|||||||
|
|
||||||
**Purpose**: Documentation, migration guidance, validation, and cleanup across all stories.
|
**Purpose**: Documentation, migration guidance, validation, and cleanup across all stories.
|
||||||
|
|
||||||
- [ ] T063 [P] Update jobs active-domain command/event/diagnostics guidance in docs/capability-domains.md
|
- [X] T063 [P] Update jobs active-domain command/event/diagnostics guidance in docs/capability-domains.md
|
||||||
- [ ] T064 [P] Update jobs migration status and removal gates in docs/capability-roadmap.md
|
- [X] T064 [P] Update jobs migration status and removal gates in docs/capability-roadmap.md
|
||||||
- [ ] T065 [P] Update jobs privileged provider-coordinator row and outcomes in docs/capability-safety-matrix.md
|
- [X] T065 [P] Update jobs privileged provider-coordinator row and outcomes in docs/capability-safety-matrix.md
|
||||||
- [ ] T066 [P] Add jobs provider/requester manifest and dispatch recipe in docs/capability-recipes.md
|
- [X] T066 [P] Add jobs provider/requester manifest and dispatch recipe in docs/capability-recipes.md
|
||||||
- [ ] T067 [P] Add changelog entry for the jobs control plane in CHANGELOG.md
|
- [X] T067 [P] Add changelog entry for the jobs control plane in CHANGELOG.md
|
||||||
- [ ] T068 Run syntax checks for static/capabilities.js, static/capabilities/jobs.js, and plugins/capability_inspector/screen.js
|
- [X] T068 Run syntax checks for static/capabilities.js, static/capabilities/jobs.js, and plugins/capability_inspector/screen.js
|
||||||
- [ ] T069 Run focused JS validation with npm run test:js and confirm jobs tests pass in tests/js/jobs_domain.test.js, tests/js/jobs_scheduling.test.js, tests/js/jobs_diagnostics.test.js, tests/js/jobs_compat.test.js, and tests/js/capability_inspector_render.test.js
|
- [X] T069 Run focused JS validation with npm run test:js and confirm jobs tests pass in tests/js/jobs_domain.test.js, tests/js/jobs_scheduling.test.js, tests/js/jobs_diagnostics.test.js, tests/js/jobs_compat.test.js, and tests/js/capability_inspector_render.test.js
|
||||||
- [ ] T070 Run focused pytest regression if diagnostics/plugin/redaction Python surfaces changed in tests/test_diagnostics_bundle.py, tests/test_diagnostics_redact.py, tests/test_plugins.py, and tests/test_plugin_runtime_idempotence.py
|
- [X] T070 Run focused pytest regression if diagnostics/plugin/redaction Python surfaces changed in tests/test_diagnostics_bundle.py, tests/test_diagnostics_redact.py, tests/test_plugins.py, and tests/test_plugin_runtime_idempotence.py
|
||||||
- [ ] T071 Run browser console smoke after UI wiring with tests/browser/check-errors.spec.ts
|
- [X] T071 Run browser console smoke after UI wiring with tests/browser/check-errors.spec.ts
|
||||||
- [ ] T072 Verify quickstart scenarios and update any discovered validation notes in specs/009-jobs/quickstart.md
|
- [X] T072 Verify quickstart scenarios and update any discovered validation notes in specs/009-jobs/quickstart.md
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
+12
-6
@@ -72,9 +72,9 @@
|
|||||||
const OUTCOMES = new Set([
|
const OUTCOMES = new Set([
|
||||||
'passed', 'transformed', 'handled', 'denied', 'degraded', 'failed',
|
'passed', 'transformed', 'handled', 'denied', 'degraded', 'failed',
|
||||||
'short-circuited', 'overridden', 'no-owner', 'no-handler',
|
'short-circuited', 'overridden', 'no-owner', 'no-handler',
|
||||||
'unsupported-command', 'incompatible', 'incompatible-version',
|
'unsupported-command', 'unsupported-operation', 'incompatible', 'incompatible-version',
|
||||||
'unavailable', 'provider-selection-required', 'user-action-required', 'no-target',
|
'unavailable', 'provider-selection-required', 'user-action-required', 'no-target',
|
||||||
'stale', 'cancelled', 'stopped',
|
'stale', 'queued', 'validation-failed', 'cancelled', 'completed', 'timeout', 'retry-started', 'stopped',
|
||||||
]);
|
]);
|
||||||
const MAX_DECISIONS = 100;
|
const MAX_DECISIONS = 100;
|
||||||
const MAX_SNAPSHOT_BYTES = 64 * 1024;
|
const MAX_SNAPSHOT_BYTES = 64 * 1024;
|
||||||
@@ -88,7 +88,6 @@
|
|||||||
'ui.player-panels',
|
'ui.player-panels',
|
||||||
'ui.player-overlays',
|
'ui.player-overlays',
|
||||||
'plugins',
|
'plugins',
|
||||||
'jobs',
|
|
||||||
'midi-control',
|
'midi-control',
|
||||||
'tempo-clock',
|
'tempo-clock',
|
||||||
]);
|
]);
|
||||||
@@ -110,6 +109,7 @@
|
|||||||
'audio-mix': Object.freeze({ lifecycle: 'active', label: 'Active contract', tone: 'clean', summary: 'Core-coordinated song route, fader, participant, and analyser inspection surface.' }),
|
'audio-mix': Object.freeze({ lifecycle: 'active', label: 'Active contract', tone: 'clean', summary: 'Core-coordinated song route, fader, participant, and analyser inspection surface.' }),
|
||||||
'audio-monitoring': Object.freeze({ lifecycle: 'active', label: 'Active contract', tone: 'clean', summary: 'Core-coordinated monitoring lifecycle, availability, consent, and bridge diagnostics.' }),
|
'audio-monitoring': Object.freeze({ lifecycle: 'active', label: 'Active contract', tone: 'clean', summary: 'Core-coordinated monitoring lifecycle, availability, consent, and bridge diagnostics.' }),
|
||||||
library: Object.freeze({ lifecycle: 'active', label: 'Active contract', tone: 'clean', summary: 'Current local and plugin-provided library source selection and sync surface.' }),
|
library: Object.freeze({ lifecycle: 'active', label: 'Active contract', tone: 'clean', summary: 'Current local and plugin-provided library source selection and sync surface.' }),
|
||||||
|
jobs: Object.freeze({ lifecycle: 'active', label: 'Active contract', tone: 'clean', summary: 'Privileged provider-coordinator control plane for long-running work, scheduling, recovery, bridges, and redaction-safe diagnostics.' }),
|
||||||
playback: Object.freeze({ lifecycle: 'active', label: 'Active contract', tone: 'clean', summary: 'Core-coordinated song transport, timing, loop, route, requester, bridge, and diagnostics surface.' }),
|
playback: Object.freeze({ lifecycle: 'active', label: 'Active contract', tone: 'clean', summary: 'Core-coordinated song transport, timing, loop, route, requester, bridge, and diagnostics surface.' }),
|
||||||
pipeline: Object.freeze({ lifecycle: 'diagnostic', label: 'Graph controls', tone: 'info', summary: 'Capability graph operations: resolve, inspect, validate, and enable or disable participants.' }),
|
pipeline: Object.freeze({ lifecycle: 'diagnostic', label: 'Graph controls', tone: 'info', summary: 'Capability graph operations: resolve, inspect, validate, and enable or disable participants.' }),
|
||||||
stems: Object.freeze({ lifecycle: 'active', label: 'Active contract', tone: 'clean', summary: 'Core-coordinated stem automation, restore, manual override, and compatibility bridge surface backed by the active Stems provider.' }),
|
stems: Object.freeze({ lifecycle: 'active', label: 'Active contract', tone: 'clean', summary: 'Core-coordinated stem automation, restore, manual override, and compatibility bridge surface backed by the active Stems provider.' }),
|
||||||
@@ -832,7 +832,7 @@
|
|||||||
|
|
||||||
function _finalOutcome(decisions) {
|
function _finalOutcome(decisions) {
|
||||||
if (!decisions.length) return 'degraded';
|
if (!decisions.length) return 'degraded';
|
||||||
const terminal = decisions.find(d => ['denied', 'failed', 'short-circuited', 'handled', 'degraded', 'overridden', 'no-owner', 'no-handler', 'no-target', 'unsupported-command', 'incompatible', 'incompatible-version', 'unavailable', 'provider-selection-required', 'user-action-required', 'stale', 'cancelled', 'stopped'].includes(d.outcome));
|
const terminal = decisions.find(d => ['denied', 'failed', 'short-circuited', 'handled', 'degraded', 'overridden', 'no-owner', 'no-handler', 'no-target', 'unsupported-command', 'unsupported-operation', 'incompatible', 'incompatible-version', 'unavailable', 'provider-selection-required', 'user-action-required', 'stale', 'queued', 'validation-failed', 'cancelled', 'completed', 'timeout', 'retry-started', 'stopped'].includes(d.outcome));
|
||||||
return terminal ? terminal.outcome : decisions[decisions.length - 1].outcome;
|
return terminal ? terminal.outcome : decisions[decisions.length - 1].outcome;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -986,7 +986,7 @@
|
|||||||
commandContext.payload = decision.payload;
|
commandContext.payload = decision.payload;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (['denied', 'failed', 'short-circuited', 'handled', 'degraded', 'overridden', 'no-owner', 'no-handler', 'no-target', 'unsupported-command', 'incompatible', 'incompatible-version', 'unavailable', 'provider-selection-required', 'user-action-required', 'stale', 'cancelled', 'stopped'].includes(decision.outcome)) break;
|
if (['denied', 'failed', 'short-circuited', 'handled', 'degraded', 'overridden', 'no-owner', 'no-handler', 'no-target', 'unsupported-command', 'unsupported-operation', 'incompatible', 'incompatible-version', 'unavailable', 'provider-selection-required', 'user-action-required', 'stale', 'queued', 'validation-failed', 'cancelled', 'completed', 'timeout', 'retry-started', 'stopped'].includes(decision.outcome)) break;
|
||||||
}
|
}
|
||||||
if (!decisions.length) {
|
if (!decisions.length) {
|
||||||
const reason = `No provider handled ${capabilityName}.${commandName}`;
|
const reason = `No provider handled ${capabilityName}.${commandName}`;
|
||||||
@@ -1005,7 +1005,7 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
const outcome = _finalOutcome(decisions);
|
const outcome = _finalOutcome(decisions);
|
||||||
const terminalDecision = decisions.find(d => ['denied', 'failed', 'short-circuited', 'handled', 'degraded', 'overridden', 'no-owner', 'no-handler', 'no-target', 'unsupported-command', 'incompatible', 'incompatible-version', 'unavailable', 'provider-selection-required', 'user-action-required', 'stale', 'cancelled', 'stopped'].includes(d.outcome))
|
const terminalDecision = decisions.find(d => ['denied', 'failed', 'short-circuited', 'handled', 'degraded', 'overridden', 'no-owner', 'no-handler', 'no-target', 'unsupported-command', 'unsupported-operation', 'incompatible', 'incompatible-version', 'unavailable', 'provider-selection-required', 'user-action-required', 'stale', 'queued', 'validation-failed', 'cancelled', 'completed', 'timeout', 'retry-started', 'stopped'].includes(d.outcome))
|
||||||
|| decisions[decisions.length - 1];
|
|| decisions[decisions.length - 1];
|
||||||
return {
|
return {
|
||||||
capability: capabilityName,
|
capability: capabilityName,
|
||||||
@@ -1317,13 +1317,19 @@
|
|||||||
if (result.outcome === 'no-handler') return 'no-handler';
|
if (result.outcome === 'no-handler') return 'no-handler';
|
||||||
if (result.outcome === 'no-target') return 'no-target';
|
if (result.outcome === 'no-target') return 'no-target';
|
||||||
if (result.outcome === 'unsupported-command') return 'unsupported-command';
|
if (result.outcome === 'unsupported-command') return 'unsupported-command';
|
||||||
|
if (result.outcome === 'unsupported-operation') return 'unsupported-operation';
|
||||||
if (result.outcome === 'incompatible') return 'incompatible';
|
if (result.outcome === 'incompatible') return 'incompatible';
|
||||||
if (result.outcome === 'incompatible-version') return 'incompatible-version';
|
if (result.outcome === 'incompatible-version') return 'incompatible-version';
|
||||||
if (result.outcome === 'unavailable') return 'unavailable';
|
if (result.outcome === 'unavailable') return 'unavailable';
|
||||||
if (result.outcome === 'provider-selection-required') return 'provider-selection-required';
|
if (result.outcome === 'provider-selection-required') return 'provider-selection-required';
|
||||||
if (result.outcome === 'user-action-required') return 'user-action-required';
|
if (result.outcome === 'user-action-required') return 'user-action-required';
|
||||||
if (result.outcome === 'stale') return 'stale';
|
if (result.outcome === 'stale') return 'stale';
|
||||||
|
if (result.outcome === 'queued') return 'queued';
|
||||||
|
if (result.outcome === 'validation-failed') return 'validation-failed';
|
||||||
if (result.outcome === 'cancelled') return 'cancelled';
|
if (result.outcome === 'cancelled') return 'cancelled';
|
||||||
|
if (result.outcome === 'completed') return 'completed';
|
||||||
|
if (result.outcome === 'timeout') return 'timeout';
|
||||||
|
if (result.outcome === 'retry-started') return 'retry-started';
|
||||||
if (result.outcome === 'stopped') return 'stopped';
|
if (result.outcome === 'stopped') return 'stopped';
|
||||||
if (result.outcome === 'denied' || result.outcome === 'short-circuited') return 'blocked';
|
if (result.outcome === 'denied' || result.outcome === 'short-circuited') return 'blocked';
|
||||||
if (result.outcome === 'failed') return 'error';
|
if (result.outcome === 'failed') return 'error';
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -24,6 +24,7 @@
|
|||||||
<script src="/static/capabilities.js"></script>
|
<script src="/static/capabilities.js"></script>
|
||||||
<script src="/static/capabilities/library.js"></script>
|
<script src="/static/capabilities/library.js"></script>
|
||||||
<script src="/static/capabilities/tuning.js"></script>
|
<script src="/static/capabilities/tuning.js"></script>
|
||||||
|
<script src="/static/capabilities/jobs.js"></script>
|
||||||
<script src="/static/capabilities/audio-session.js"></script>
|
<script src="/static/capabilities/audio-session.js"></script>
|
||||||
<script src="/static/capabilities/audio-effects.js"></script>
|
<script src="/static/capabilities/audio-effects.js"></script>
|
||||||
<script src="/static/capabilities/playback.js"></script>
|
<script src="/static/capabilities/playback.js"></script>
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ test('diagnostics export expected compatibility shim surfaces', () => {
|
|||||||
assert.equal(expected.some(entry => entry.capability === 'jobs'), false);
|
assert.equal(expected.some(entry => entry.capability === 'jobs'), false);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('diagnostics include active playback but exclude deferred and documentation-only future core domains', () => {
|
test('diagnostics include active playback and jobs but exclude deferred future core domains', () => {
|
||||||
const window = loadCapabilities();
|
const window = loadCapabilities();
|
||||||
const api = window.slopsmith.capabilities;
|
const api = window.slopsmith.capabilities;
|
||||||
api.registerParticipant('playback_probe', {
|
api.registerParticipant('playback_probe', {
|
||||||
@@ -74,14 +74,14 @@ test('diagnostics include active playback but exclude deferred and documentation
|
|||||||
const playback = pipelines.find(entry => entry.name === 'playback');
|
const playback = pipelines.find(entry => entry.name === 'playback');
|
||||||
assert.ok(playback, 'playback should be part of the active runtime graph when participants register');
|
assert.ok(playback, 'playback should be part of the active runtime graph when participants register');
|
||||||
assert.equal(playback.review.lifecycle, 'active');
|
assert.equal(playback.review.lifecycle, 'active');
|
||||||
|
const jobs = pipelines.find(entry => entry.name === 'jobs');
|
||||||
|
assert.ok(jobs, 'jobs should be part of the active runtime graph when participants register');
|
||||||
|
assert.equal(jobs.review.lifecycle, 'active');
|
||||||
|
|
||||||
// `visualization` (cap:6 slice) and `note-detection` (spec 009 slice)
|
// `visualization` (cap:6 slice) and `note-detection` (spec 009 slice)
|
||||||
// left this list when their domains were promoted.
|
// left this list when their domains were promoted.
|
||||||
const futureDomains = [
|
const futureDomains = [
|
||||||
'ui.navigation', 'ui.plugin-screens', 'settings',
|
'ui.navigation', 'ui.plugin-screens', 'settings', 'backend.routes', 'ui.player-controls', 'ui.player-panels', 'ui.player-overlays', 'plugins', 'midi-control', 'tempo-clock',
|
||||||
'backend.routes', 'ui.player-controls',
|
|
||||||
'ui.player-panels', 'ui.player-overlays', 'plugins', 'jobs', 'midi-control',
|
|
||||||
'tempo-clock',
|
|
||||||
];
|
];
|
||||||
|
|
||||||
for (const domain of futureDomains) {
|
for (const domain of futureDomains) {
|
||||||
|
|||||||
@@ -65,6 +65,9 @@ function loadInspector(snapshot, options = {}) {
|
|||||||
playback: options.playbackSnapshot ? {
|
playback: options.playbackSnapshot ? {
|
||||||
snapshot: () => options.playbackSnapshot,
|
snapshot: () => options.playbackSnapshot,
|
||||||
} : undefined,
|
} : undefined,
|
||||||
|
jobs: options.jobsSnapshot ? {
|
||||||
|
snapshot: () => options.jobsSnapshot,
|
||||||
|
} : undefined,
|
||||||
},
|
},
|
||||||
navigator: { clipboard: { writeText: async () => {} } },
|
navigator: { clipboard: { writeText: async () => {} } },
|
||||||
document: {
|
document: {
|
||||||
@@ -114,6 +117,41 @@ test('capability inspector renders playback session route loop bridges and outco
|
|||||||
assert.match(content, /seek:completed/);
|
assert.match(content, /seek:completed/);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('capability inspector renders jobs support state and active domain graph', () => {
|
||||||
|
const snapshot = {
|
||||||
|
pipelines: [{ name: 'jobs', review: { lifecycle: 'active', label: 'Active contract', tone: 'clean', summary: 'Jobs control plane.' }, participants: [{ pluginId: 'core.jobs', kind: 'provider-coordinator', roles: ['owner'], commands: ['enqueue', 'list', 'inspect'], operations: ['job.enqueue'], events: ['queued', 'completed'], runtime: true, availability: 'available', ownership: 'multi-provider', safety: 'privileged' }], conflicts: [] }],
|
||||||
|
participants: [{ pluginId: 'core.jobs' }],
|
||||||
|
compatibilityShims: [],
|
||||||
|
expectedCompatibilityShims: [],
|
||||||
|
};
|
||||||
|
const jobsSnapshot = {
|
||||||
|
schema: 'slopsmith.jobs.diagnostics.v1',
|
||||||
|
providers: [{ providerId: 'provider.cache', label: 'Cache Builder', availability: 'available', jobTypes: ['cache-build'] }],
|
||||||
|
selectedProviders: [{ jobType: 'cache-build', providerId: 'provider.cache', source: 'user-selected' }],
|
||||||
|
jobs: {
|
||||||
|
active: [{ jobId: 'job-1', safeLabel: 'Build cache', providerId: 'provider.cache', state: 'running', progress: { percent: 42 } }],
|
||||||
|
queued: [{ jobId: 'job-2', safeLabel: 'Later cache', providerId: 'provider.cache', priority: 'background-maintenance' }],
|
||||||
|
paused: [],
|
||||||
|
recentTerminal: [{ jobId: 'job-0', safeLabel: 'Old cache', providerId: 'provider.cache', state: 'completed', terminalOutcome: { status: 'completed' } }],
|
||||||
|
},
|
||||||
|
outcomes: [{ operation: 'enqueue', status: 'queued' }, { operation: 'complete', status: 'completed' }],
|
||||||
|
bridgeHits: [{ bridgeId: 'jobs.legacy-plugin-queue', operation: 'enqueue', diagnosticsOnly: true }],
|
||||||
|
};
|
||||||
|
const { elements } = loadInspector(snapshot, { jobsSnapshot });
|
||||||
|
const content = elements.get('capability-inspector-content').innerHTML;
|
||||||
|
const filter = elements.get('capability-inspector-filter').innerHTML;
|
||||||
|
|
||||||
|
assert.match(content, /data-jobs-support/);
|
||||||
|
assert.match(content, /Providers: Cache Builder:available:cache-build/);
|
||||||
|
assert.match(content, /Selected: cache-build:provider.cache:user-selected/);
|
||||||
|
assert.match(content, /Active: Build cache:provider.cache:42%/);
|
||||||
|
assert.match(content, /Queued: Later cache:provider.cache:background-maintenance/);
|
||||||
|
assert.match(content, /Bridges: jobs.legacy-plugin-queue:enqueue:diagnostics-only/);
|
||||||
|
assert.match(content, /Outcomes: enqueue:queued, complete:completed/);
|
||||||
|
assert.match(content, /data-domain-graph="jobs"/);
|
||||||
|
assert.match(filter, /jobs/);
|
||||||
|
});
|
||||||
|
|
||||||
test('capability inspector renders shims inside their capability domain', () => {
|
test('capability inspector renders shims inside their capability domain', () => {
|
||||||
const snapshot = {
|
const snapshot = {
|
||||||
pipelines: [
|
pipelines: [
|
||||||
|
|||||||
@@ -0,0 +1,58 @@
|
|||||||
|
const { test } = require('node:test');
|
||||||
|
const assert = require('node:assert/strict');
|
||||||
|
const { loadJobs, dispatch, makeProvider, enqueuePayload, diagnosticsSnapshot, captureEvents } = require('./jobs_test_harness');
|
||||||
|
|
||||||
|
test('legacy bridge hits are recorded as diagnostics-only compatibility shims', async () => {
|
||||||
|
const window = loadJobs();
|
||||||
|
const events = captureEvents(window);
|
||||||
|
const { provider } = makeProvider({ providerId: 'provider.bridge' });
|
||||||
|
await dispatch(window, 'register-provider', { provider });
|
||||||
|
const enqueued = await dispatch(window, 'enqueue', enqueuePayload({ providerId: 'provider.bridge', logicalJobKey: 'legacy-key' }));
|
||||||
|
|
||||||
|
const result = await dispatch(window, 'record-bridge-hit', {
|
||||||
|
bridgeId: 'jobs.legacy-plugin-queue',
|
||||||
|
legacySurface: 'legacy-plugin-queue',
|
||||||
|
pluginId: 'legacy_plugin',
|
||||||
|
operation: 'enqueue',
|
||||||
|
logicalJobKey: 'legacy-key',
|
||||||
|
safeReason: 'legacy queue adapter used',
|
||||||
|
});
|
||||||
|
const runtimeSnapshot = window.slopsmith.capabilities.snapshotDiagnostics();
|
||||||
|
const diagnostics = diagnosticsSnapshot(window);
|
||||||
|
|
||||||
|
assert.equal(result.status, 'applied');
|
||||||
|
assert.equal(result.payload.bridge.jobId, enqueued.payload.job.jobId);
|
||||||
|
assert.equal(result.payload.bridge.diagnosticsOnly, true);
|
||||||
|
assert.ok(runtimeSnapshot.compatibilityShims.some(shim => shim.capability === 'jobs' && shim.status === 'used'));
|
||||||
|
assert.equal(diagnostics.bridgeHits.length, 1);
|
||||||
|
assert.ok(events.some(event => event.event === 'bridge-hit'));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('duplicate logical jobs are suppressed and remain tied to the active job', async () => {
|
||||||
|
const window = loadJobs();
|
||||||
|
const { provider } = makeProvider({ providerId: 'provider.duplicate' });
|
||||||
|
await dispatch(window, 'register-provider', { provider });
|
||||||
|
|
||||||
|
const first = await dispatch(window, 'enqueue', enqueuePayload({ providerId: 'provider.duplicate', logicalJobKey: 'same-job' }));
|
||||||
|
const second = await dispatch(window, 'enqueue', enqueuePayload({ providerId: 'provider.duplicate', logicalJobKey: 'same-job' }));
|
||||||
|
|
||||||
|
assert.equal(second.status, 'applied');
|
||||||
|
assert.equal(second.payload.duplicate, true);
|
||||||
|
assert.equal(second.payload.job.jobId, first.payload.job.jobId);
|
||||||
|
assert.equal(diagnosticsSnapshot(window).jobs.active.length, 1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('unsupported provider actions return unsupported-operation instead of invoking callbacks', async () => {
|
||||||
|
const window = loadJobs();
|
||||||
|
const { calls, provider } = makeProvider({ providerId: 'provider.limited', actions: ['enqueue', 'inspect'] });
|
||||||
|
await dispatch(window, 'register-provider', { provider });
|
||||||
|
const enqueued = await dispatch(window, 'enqueue', enqueuePayload({ providerId: 'provider.limited' }));
|
||||||
|
calls.length = 0;
|
||||||
|
|
||||||
|
const pause = await dispatch(window, 'pause', { jobId: enqueued.payload.job.jobId });
|
||||||
|
const retry = await dispatch(window, 'retry', { jobId: enqueued.payload.job.jobId, authorization: 'user-action' });
|
||||||
|
|
||||||
|
assert.equal(pause.status, 'unsupported-operation');
|
||||||
|
assert.equal(retry.status, 'stale');
|
||||||
|
assert.equal(calls.length, 0);
|
||||||
|
});
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
const { test } = require('node:test');
|
||||||
|
const assert = require('node:assert/strict');
|
||||||
|
const { loadJobs, dispatch, makeProvider, enqueuePayload, diagnosticsSnapshot, storageEntries } = require('./jobs_test_harness');
|
||||||
|
|
||||||
|
test('diagnostics schema redacts raw payloads, paths, command lines, and provider-private fields', async () => {
|
||||||
|
const window = loadJobs();
|
||||||
|
const { provider } = makeProvider();
|
||||||
|
await dispatch(window, 'register-provider', { provider });
|
||||||
|
const enqueued = await dispatch(window, 'enqueue', enqueuePayload({
|
||||||
|
safeLabel: 'Generate cache',
|
||||||
|
target: { path: '/Users/example/Music/Secret Artist - Secret Song_p.psarc', filename: 'Secret Song_p.psarc' },
|
||||||
|
inputs: { token: 'abc123', rawPayload: 'never export', commandLine: 'ffmpeg -i secret.wav out.ogg', safeFingerprint: 'fingerprint-public' },
|
||||||
|
}));
|
||||||
|
window.slopsmith.jobs.log(provider.providerId, enqueued.payload.job.jobId, 'ran ffmpeg -i /Users/example/secret.wav with token=abc123');
|
||||||
|
window.slopsmith.jobs.fail(provider.providerId, enqueued.payload.job.jobId, { safeReason: 'failed near /Users/example/private/path', retryable: true });
|
||||||
|
|
||||||
|
const json = JSON.stringify(diagnosticsSnapshot(window));
|
||||||
|
assert.match(json, /Generate cache/);
|
||||||
|
assert.match(json, /fingerprint-public/);
|
||||||
|
assert.doesNotMatch(json, /Secret Artist|Secret Song|secret\.wav|abc123|rawPayload|commandLine|never export|ffmpeg -i/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('recoverable job references are the only active state persisted across reloads', async () => {
|
||||||
|
const window = loadJobs();
|
||||||
|
const { provider } = makeProvider({ providerId: 'provider.recover', recoverySupport: { queued: true, running: true, paused: false } });
|
||||||
|
await dispatch(window, 'register-provider', { provider });
|
||||||
|
await dispatch(window, 'enqueue', enqueuePayload({ logicalJobKey: 'recoverable-running', safeLabel: 'Recoverable' }));
|
||||||
|
|
||||||
|
const entries = storageEntries(window);
|
||||||
|
assert.ok(entries['slopsmith.jobs.recoverableRefs.v1']);
|
||||||
|
assert.doesNotMatch(entries['slopsmith.jobs.recoverableRefs.v1'], /operationHandlers|rawPayload|token/);
|
||||||
|
|
||||||
|
const sameWindow = window;
|
||||||
|
sameWindow.slopsmith.jobs.resetForTests({ clearStorage: false });
|
||||||
|
assert.equal(sameWindow.slopsmith.jobs._test.pendingRecoverableRefs.size, 1);
|
||||||
|
await dispatch(sameWindow, 'register-provider', { provider });
|
||||||
|
const snapshot = diagnosticsSnapshot(sameWindow);
|
||||||
|
assert.equal(snapshot.jobs.active.length + snapshot.jobs.queued.length, 1);
|
||||||
|
assert.equal(snapshot.jobs.active[0]?.safeLabel || snapshot.jobs.queued[0]?.safeLabel, 'Recoverable');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('reload marks non-recoverable jobs orphaned or provider-unavailable without restoring raw payloads', async () => {
|
||||||
|
const window = loadJobs();
|
||||||
|
const { provider } = makeProvider({ providerId: 'provider.no-recover', recoverySupport: { queued: false, running: false, paused: false } });
|
||||||
|
await dispatch(window, 'register-provider', { provider });
|
||||||
|
const enqueued = await dispatch(window, 'enqueue', enqueuePayload({ providerId: 'provider.no-recover', logicalJobKey: 'no-recover' }));
|
||||||
|
|
||||||
|
window.slopsmith.jobs.simulateReload();
|
||||||
|
const inspected = await dispatch(window, 'inspect', { jobId: enqueued.payload.job.jobId });
|
||||||
|
|
||||||
|
assert.equal(inspected.payload.job.state, 'orphaned');
|
||||||
|
assert.equal(inspected.payload.job.terminalOutcome.retryable, false);
|
||||||
|
assert.doesNotMatch(JSON.stringify(inspected.payload.job), /operationHandlers|rawPayload/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('diagnostics enforce per-job history and bounded snapshot size with terminal minimum retained', async () => {
|
||||||
|
const window = loadJobs();
|
||||||
|
const { provider } = makeProvider({ capacity: { maxRunning: 1, maxQueued: 100 } });
|
||||||
|
await dispatch(window, 'register-provider', { provider });
|
||||||
|
let lastJobId = null;
|
||||||
|
|
||||||
|
for (let index = 0; index < 8; index += 1) {
|
||||||
|
const enqueued = await dispatch(window, 'enqueue', enqueuePayload({ logicalJobKey: `terminal-${index}`, safeLabel: `Terminal ${index}` }));
|
||||||
|
lastJobId = enqueued.payload.job.jobId;
|
||||||
|
for (let line = 0; line < 80; line += 1) window.slopsmith.jobs.log(provider.providerId, lastJobId, `line ${line} /Users/example/private/file-${line}.psarc`);
|
||||||
|
window.slopsmith.jobs.complete(provider.providerId, lastJobId, { resultSummary: 'done' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const snapshot = diagnosticsSnapshot(window);
|
||||||
|
assert.ok(snapshot.snapshotBytes <= snapshot.limits.snapshotBudgetBytes + 1024);
|
||||||
|
assert.ok(snapshot.jobs.recentTerminal.length >= 5);
|
||||||
|
assert.ok(snapshot.jobs.recentTerminal.every(job => job.history.length <= 50));
|
||||||
|
});
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
const { test } = require('node:test');
|
||||||
|
const assert = require('node:assert/strict');
|
||||||
|
const { loadJobs, dispatch, makeProvider, enqueuePayload, diagnosticsSnapshot, diagnosticsContributions } = require('./jobs_test_harness');
|
||||||
|
|
||||||
|
test('jobs domain is active and registers the core provider coordinator owner', async () => {
|
||||||
|
const window = loadJobs();
|
||||||
|
const snapshot = window.slopsmith.capabilities.snapshotDiagnostics();
|
||||||
|
const jobs = snapshot.pipelines.find(pipeline => pipeline.name === 'jobs');
|
||||||
|
|
||||||
|
assert.ok(jobs);
|
||||||
|
assert.equal(jobs.review.lifecycle, 'active');
|
||||||
|
assert.equal(jobs.review.tone, 'clean');
|
||||||
|
assert.ok(jobs.participants.some(participant => participant.pluginId === 'core.jobs' && participant.kind === 'provider-coordinator' && participant.safety === 'privileged'));
|
||||||
|
|
||||||
|
const result = await dispatch(window, 'list-providers');
|
||||||
|
assert.equal(result.status, 'applied');
|
||||||
|
assert.equal(result.payload.providers.length, 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('provider registration exposes safe metadata and rejects incompatible versions', async () => {
|
||||||
|
const window = loadJobs();
|
||||||
|
const { provider } = makeProvider({ providerId: 'provider.cache', label: 'Cache Builder', jobTypes: ['cache-build'] });
|
||||||
|
|
||||||
|
const registered = await dispatch(window, 'register-provider', { provider });
|
||||||
|
assert.equal(registered.status, 'applied');
|
||||||
|
assert.equal(registered.payload.provider.providerId, 'provider.cache');
|
||||||
|
assert.deepEqual(Array.from(registered.payload.provider.jobTypes), ['cache-build']);
|
||||||
|
|
||||||
|
const incompatible = await dispatch(window, 'register-provider', { provider: { ...provider, providerId: 'provider.old', version: 2, safeReason: 'requires runtime v2' } });
|
||||||
|
assert.equal(incompatible.status, 'incompatible-version');
|
||||||
|
assert.equal(incompatible.payload.provider.availability, 'incompatible');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('privileged enqueue requires approval before provider work starts', async () => {
|
||||||
|
const window = loadJobs();
|
||||||
|
const { calls, provider } = makeProvider();
|
||||||
|
await dispatch(window, 'register-provider', { provider });
|
||||||
|
|
||||||
|
const result = await dispatch(window, 'enqueue', enqueuePayload({ authorization: 'background' }));
|
||||||
|
|
||||||
|
assert.equal(result.status, 'user-action-required');
|
||||||
|
assert.equal(calls.length, 0);
|
||||||
|
assert.equal(diagnosticsSnapshot(window).jobs.active.length, 0);
|
||||||
|
assert.equal(diagnosticsContributions(window).jobs.schema, 'slopsmith.jobs.diagnostics.v1');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('approved enqueue queues and starts with redaction-safe public job fields', async () => {
|
||||||
|
const window = loadJobs();
|
||||||
|
const { calls, provider } = makeProvider({ capacity: { maxRunning: 1, maxQueued: 10 } });
|
||||||
|
await dispatch(window, 'register-provider', { provider });
|
||||||
|
|
||||||
|
const result = await dispatch(window, 'enqueue', enqueuePayload({ target: { path: '/Users/example/DLC/Secret.psarc' }, inputs: { token: 'secret', safeFingerprint: 'fingerprint-1' } }));
|
||||||
|
const job = result.payload.job;
|
||||||
|
|
||||||
|
assert.equal(result.status, 'applied');
|
||||||
|
assert.equal(job.state, 'running');
|
||||||
|
assert.equal(job.targetRef.startsWith('target-'), true);
|
||||||
|
assert.equal(job.inputFingerprint, 'fingerprint-1');
|
||||||
|
assert.equal(calls.length, 1);
|
||||||
|
assert.doesNotMatch(JSON.stringify(diagnosticsSnapshot(window)), /Secret\.psarc|token|secret/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('list and inspect are prompt-free and do not invoke provider callbacks', async () => {
|
||||||
|
const window = loadJobs();
|
||||||
|
const { calls, provider } = makeProvider();
|
||||||
|
await dispatch(window, 'register-provider', { provider });
|
||||||
|
const enqueued = await dispatch(window, 'enqueue', enqueuePayload());
|
||||||
|
calls.length = 0;
|
||||||
|
|
||||||
|
const listed = await dispatch(window, 'list');
|
||||||
|
const inspected = await dispatch(window, 'inspect', { jobId: enqueued.payload.job.jobId });
|
||||||
|
|
||||||
|
assert.equal(listed.status, 'applied');
|
||||||
|
assert.equal(inspected.status, 'applied');
|
||||||
|
assert.equal(listed.payload.jobs.length, 1);
|
||||||
|
assert.equal(inspected.payload.job.jobId, enqueued.payload.job.jobId);
|
||||||
|
assert.equal(calls.length, 0);
|
||||||
|
});
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
const { test } = require('node:test');
|
||||||
|
const assert = require('node:assert/strict');
|
||||||
|
const { loadJobs, dispatch, makeProvider, enqueuePayload, captureEvents, diagnosticsSnapshot } = require('./jobs_test_harness');
|
||||||
|
|
||||||
|
test('multiple providers require explicit selection before scheduling', async () => {
|
||||||
|
const window = loadJobs();
|
||||||
|
await dispatch(window, 'register-provider', { provider: makeProvider({ providerId: 'provider.a' }).provider });
|
||||||
|
await dispatch(window, 'register-provider', { provider: makeProvider({ providerId: 'provider.b' }).provider });
|
||||||
|
|
||||||
|
const blocked = await dispatch(window, 'enqueue', enqueuePayload());
|
||||||
|
assert.equal(blocked.status, 'provider-selection-required');
|
||||||
|
|
||||||
|
window.slopsmith.jobs.setSelectedProvider('transcode', 'provider.b');
|
||||||
|
const enqueued = await dispatch(window, 'enqueue', enqueuePayload({ logicalJobKey: 'selected-job' }));
|
||||||
|
assert.equal(enqueued.status, 'applied');
|
||||||
|
assert.equal(enqueued.payload.job.providerId, 'provider.b');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('provider capacity starts one job and keeps the rest queued in priority order', async () => {
|
||||||
|
const window = loadJobs();
|
||||||
|
const { provider } = makeProvider({ capacity: { maxRunning: 1, maxQueued: 10 } });
|
||||||
|
await dispatch(window, 'register-provider', { provider });
|
||||||
|
|
||||||
|
const first = await dispatch(window, 'enqueue', enqueuePayload({ logicalJobKey: 'first', priority: 'background-maintenance' }));
|
||||||
|
const second = await dispatch(window, 'enqueue', enqueuePayload({ logicalJobKey: 'second', priority: 'background-maintenance', safeLabel: 'Second' }));
|
||||||
|
const third = await dispatch(window, 'enqueue', enqueuePayload({ logicalJobKey: 'third', priority: 'user-approved-interactive', safeLabel: 'Third' }));
|
||||||
|
|
||||||
|
assert.equal(first.payload.job.state, 'running');
|
||||||
|
assert.equal(second.payload.job.state, 'queued');
|
||||||
|
assert.equal(third.payload.job.state, 'queued');
|
||||||
|
|
||||||
|
window.slopsmith.jobs.complete(provider.providerId, first.payload.job.jobId, { resultSummary: 'done' });
|
||||||
|
const snapshot = diagnosticsSnapshot(window);
|
||||||
|
assert.equal(snapshot.jobs.active.length, 1);
|
||||||
|
assert.equal(snapshot.jobs.active[0].safeLabel, 'Third');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('progress, completion, and terminal retention are reflected in events and diagnostics', async () => {
|
||||||
|
const window = loadJobs();
|
||||||
|
const events = captureEvents(window);
|
||||||
|
const { provider } = makeProvider();
|
||||||
|
await dispatch(window, 'register-provider', { provider });
|
||||||
|
const enqueued = await dispatch(window, 'enqueue', enqueuePayload());
|
||||||
|
|
||||||
|
window.slopsmith.jobs.updateProgress(provider.providerId, enqueued.payload.job.jobId, { percent: 33, step: 'convert', message: 'Converting safely' });
|
||||||
|
window.slopsmith.jobs.complete(provider.providerId, enqueued.payload.job.jobId, { resultSummary: 'Cache ready' });
|
||||||
|
const snapshot = diagnosticsSnapshot(window);
|
||||||
|
|
||||||
|
assert.equal(snapshot.jobs.active.length, 0);
|
||||||
|
assert.equal(snapshot.jobs.recentTerminal.length, 1);
|
||||||
|
assert.equal(snapshot.jobs.recentTerminal[0].terminalOutcome.status, 'completed');
|
||||||
|
assert.ok(snapshot.jobs.recentTerminal[0].history.some(entry => entry.kind === 'progress'));
|
||||||
|
assert.ok(events.some(event => event.event === 'progress'));
|
||||||
|
assert.ok(events.some(event => event.event === 'completed'));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('cancel, pause, resume, retry, and stale transitions use canonical outcomes', async () => {
|
||||||
|
const window = loadJobs();
|
||||||
|
const { provider } = makeProvider({ capacity: { maxRunning: 1, maxQueued: 10 } });
|
||||||
|
await dispatch(window, 'register-provider', { provider });
|
||||||
|
const enqueued = await dispatch(window, 'enqueue', enqueuePayload({ logicalJobKey: 'control-job' }));
|
||||||
|
const jobId = enqueued.payload.job.jobId;
|
||||||
|
|
||||||
|
assert.equal((await dispatch(window, 'pause', { jobId })).status, 'applied');
|
||||||
|
assert.equal((await dispatch(window, 'resume', { jobId })).status, 'applied');
|
||||||
|
assert.equal((await dispatch(window, 'pause', { jobId })).status, 'applied');
|
||||||
|
assert.equal((await dispatch(window, 'cancel', { jobId })).status, 'applied');
|
||||||
|
window.slopsmith.jobs.fail(provider.providerId, jobId, { category: 'provider-failure', safeReason: 'retryable failure', retryable: true });
|
||||||
|
assert.equal((await dispatch(window, 'retry', { jobId, authorization: 'user-action' })).status, 'retry-started');
|
||||||
|
assert.equal((await dispatch(window, 'retry', { jobId, authorization: 'user-action' })).status, 'stale');
|
||||||
|
});
|
||||||
@@ -0,0 +1,159 @@
|
|||||||
|
const fs = require('node:fs');
|
||||||
|
const path = require('node:path');
|
||||||
|
const vm = require('node:vm');
|
||||||
|
const { createWindow, ROOT } = require('./capabilities_test_harness');
|
||||||
|
|
||||||
|
const CAPABILITIES_JS = path.join(ROOT, 'static', 'capabilities.js');
|
||||||
|
const JOBS_JS = path.join(ROOT, 'static', 'capabilities', 'jobs.js');
|
||||||
|
const INSPECTOR_JS = path.join(ROOT, 'plugins', 'capability_inspector', 'screen.js');
|
||||||
|
|
||||||
|
function loadJobs(options = {}) {
|
||||||
|
const window = createWindow(options);
|
||||||
|
const context = vm.createContext(window);
|
||||||
|
vm.runInContext(fs.readFileSync(CAPABILITIES_JS, 'utf8'), context, { filename: CAPABILITIES_JS });
|
||||||
|
vm.runInContext(fs.readFileSync(JOBS_JS, 'utf8'), context, { filename: JOBS_JS });
|
||||||
|
window.__vmContext = context;
|
||||||
|
return window;
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadInspector(window) {
|
||||||
|
vm.runInContext(fs.readFileSync(INSPECTOR_JS, 'utf8'), window.__vmContext, { filename: INSPECTOR_JS });
|
||||||
|
return window;
|
||||||
|
}
|
||||||
|
|
||||||
|
function captureEvents(window, capability = 'jobs') {
|
||||||
|
const events = [];
|
||||||
|
window.slopsmith.on('capability:event', event => {
|
||||||
|
if (event.detail && event.detail.capability === capability) events.push(event.detail);
|
||||||
|
});
|
||||||
|
return events;
|
||||||
|
}
|
||||||
|
|
||||||
|
function diagnosticsSnapshot(window) {
|
||||||
|
return window.slopsmith.jobs.snapshot();
|
||||||
|
}
|
||||||
|
|
||||||
|
function diagnosticsContributions(window) {
|
||||||
|
return Object.fromEntries(window.__diagnosticsContributions || new Map());
|
||||||
|
}
|
||||||
|
|
||||||
|
function storageEntries(window) {
|
||||||
|
return Object.fromEntries(window.__storage || new Map());
|
||||||
|
}
|
||||||
|
|
||||||
|
async function dispatch(window, command, payload = {}, requester = 'test') {
|
||||||
|
return window.slopsmith.capabilities.dispatch({ capability: 'jobs', command, args: payload, requester });
|
||||||
|
}
|
||||||
|
|
||||||
|
function makeProvider(overrides = {}) {
|
||||||
|
const calls = [];
|
||||||
|
const providerId = overrides.providerId || 'provider.jobs';
|
||||||
|
const handlers = overrides.operationHandlers || {};
|
||||||
|
const provider = {
|
||||||
|
providerId,
|
||||||
|
pluginId: overrides.pluginId || providerId,
|
||||||
|
label: overrides.label || 'Jobs Provider',
|
||||||
|
jobTypes: overrides.jobTypes || ['transcode'],
|
||||||
|
actions: overrides.actions || ['enqueue', 'inspect', 'cancel', 'pause', 'resume', 'retry', 'recover'],
|
||||||
|
availability: overrides.availability || 'available',
|
||||||
|
capacity: overrides.capacity || { maxRunning: 1, maxQueued: 10 },
|
||||||
|
recoverySupport: overrides.recoverySupport || { queued: true, running: true, paused: true },
|
||||||
|
version: overrides.version || 1,
|
||||||
|
safeReason: overrides.safeReason,
|
||||||
|
operationHandlers: {
|
||||||
|
'job.enqueue': request => {
|
||||||
|
calls.push(['job.enqueue', request]);
|
||||||
|
return handlers['job.enqueue'] ? handlers['job.enqueue'](request) : { outcome: 'handled' };
|
||||||
|
},
|
||||||
|
'job.cancel': request => {
|
||||||
|
calls.push(['job.cancel', request]);
|
||||||
|
return handlers['job.cancel'] ? handlers['job.cancel'](request) : { outcome: 'handled' };
|
||||||
|
},
|
||||||
|
'job.pause': request => {
|
||||||
|
calls.push(['job.pause', request]);
|
||||||
|
return handlers['job.pause'] ? handlers['job.pause'](request) : { outcome: 'handled' };
|
||||||
|
},
|
||||||
|
'job.resume': request => {
|
||||||
|
calls.push(['job.resume', request]);
|
||||||
|
return handlers['job.resume'] ? handlers['job.resume'](request) : { outcome: 'handled' };
|
||||||
|
},
|
||||||
|
'job.retry': request => {
|
||||||
|
calls.push(['job.retry', request]);
|
||||||
|
return handlers['job.retry'] ? handlers['job.retry'](request) : { outcome: 'handled' };
|
||||||
|
},
|
||||||
|
'job.recover': request => {
|
||||||
|
calls.push(['job.recover', request]);
|
||||||
|
return handlers['job.recover'] ? handlers['job.recover'](request) : { outcome: 'handled', state: overrides.recoveredState || 'queued' };
|
||||||
|
},
|
||||||
|
...handlers,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
return { calls, provider };
|
||||||
|
}
|
||||||
|
|
||||||
|
function enqueuePayload(overrides = {}) {
|
||||||
|
return {
|
||||||
|
jobType: overrides.jobType || 'transcode',
|
||||||
|
requester: overrides.requester || 'plugin.requester',
|
||||||
|
authorization: overrides.authorization || 'user-action',
|
||||||
|
priority: overrides.priority || 'user-approved-interactive',
|
||||||
|
target: overrides.target || { targetRef: 'song-1' },
|
||||||
|
inputs: overrides.inputs || { safeFingerprint: 'input-1' },
|
||||||
|
safeLabel: overrides.safeLabel || 'Build playable cache',
|
||||||
|
logicalJobKey: overrides.logicalJobKey,
|
||||||
|
providerId: overrides.providerId,
|
||||||
|
privileged: overrides.privileged,
|
||||||
|
approvalScopeKey: overrides.approvalScopeKey,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function installInspectorDom(window) {
|
||||||
|
const elements = new Map();
|
||||||
|
function element(id) {
|
||||||
|
const item = {
|
||||||
|
id,
|
||||||
|
value: '',
|
||||||
|
textContent: '',
|
||||||
|
innerHTML: '',
|
||||||
|
className: '',
|
||||||
|
dataset: {},
|
||||||
|
style: {},
|
||||||
|
classList: { add() {}, remove() {}, toggle() {}, contains() { return false; } },
|
||||||
|
addEventListener() {},
|
||||||
|
removeEventListener() {},
|
||||||
|
querySelectorAll() { return []; },
|
||||||
|
querySelector() { return null; },
|
||||||
|
closest() { return null; },
|
||||||
|
getBoundingClientRect() { return { width: 1000, height: 400, left: 0, top: 0 }; },
|
||||||
|
appendChild(child) { return child; },
|
||||||
|
};
|
||||||
|
elements.set(id, item);
|
||||||
|
return item;
|
||||||
|
}
|
||||||
|
element('capability-inspector-filter');
|
||||||
|
element('capability-inspector-content');
|
||||||
|
element('capability-inspector-empty');
|
||||||
|
element('capability-inspector-summary');
|
||||||
|
element('capability-inspector-refresh');
|
||||||
|
window.document.readyState = 'complete';
|
||||||
|
window.document.getElementById = id => elements.get(id) || null;
|
||||||
|
window.document.querySelectorAll = () => [];
|
||||||
|
window.document.addEventListener = () => {};
|
||||||
|
window.document.createElement = () => element(`created-${elements.size}`);
|
||||||
|
window.requestAnimationFrame = callback => callback();
|
||||||
|
return elements;
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
ROOT,
|
||||||
|
loadJobs,
|
||||||
|
loadInspector,
|
||||||
|
captureEvents,
|
||||||
|
diagnosticsSnapshot,
|
||||||
|
diagnosticsContributions,
|
||||||
|
storageEntries,
|
||||||
|
dispatch,
|
||||||
|
makeProvider,
|
||||||
|
enqueuePayload,
|
||||||
|
installInspectorDom,
|
||||||
|
};
|
||||||
@@ -63,8 +63,11 @@ def test_app_shell_loads_capability_registry_before_app_runtime():
|
|||||||
|
|
||||||
assert '<script src="/static/capabilities.js"></script>' in source
|
assert '<script src="/static/capabilities.js"></script>' in source
|
||||||
assert '<script src="/static/capabilities/library.js"></script>' in source
|
assert '<script src="/static/capabilities/library.js"></script>' in source
|
||||||
|
assert '<script src="/static/capabilities/jobs.js"></script>' in source
|
||||||
assert source.index('/static/diagnostics.js') < source.index('/static/capabilities.js')
|
assert source.index('/static/diagnostics.js') < source.index('/static/capabilities.js')
|
||||||
assert source.index('/static/capabilities.js') < source.index('/static/capabilities/library.js')
|
assert source.index('/static/capabilities.js') < source.index('/static/capabilities/library.js')
|
||||||
|
assert source.index('/static/capabilities/library.js') < source.index('/static/capabilities/jobs.js')
|
||||||
|
assert source.index('/static/capabilities/jobs.js') < source.index('/static/app.js')
|
||||||
assert source.index('/static/capabilities/library.js') < source.index('/static/app.js')
|
assert source.index('/static/capabilities/library.js') < source.index('/static/app.js')
|
||||||
|
|
||||||
|
|
||||||
@@ -83,7 +86,8 @@ def test_capability_runtime_overrides_do_not_mask_claims():
|
|||||||
set_enabled = source[source.index("function setParticipantEnabled("):source.index("function registerParticipants(")]
|
set_enabled = source[source.index("function setParticipantEnabled("):source.index("function registerParticipants(")]
|
||||||
reserved = source[source.index("const RESERVED_FUTURE_DOMAINS"):source.index("const RUNTIME_DOMAIN_DEFAULTS")]
|
reserved = source[source.index("const RESERVED_FUTURE_DOMAINS"):source.index("const RUNTIME_DOMAIN_DEFAULTS")]
|
||||||
|
|
||||||
assert "['denied', 'failed', 'short-circuited', 'handled', 'degraded', 'overridden', 'no-owner', 'no-handler', 'no-target', 'unsupported-command', 'incompatible', 'incompatible-version', 'unavailable', 'provider-selection-required', 'user-action-required', 'stale', 'cancelled', 'stopped'].includes(decision.outcome)" in source
|
for token in ["'unsupported-operation'", "'queued'", "'validation-failed'", "'completed'", "'timeout'", "'retry-started'"]:
|
||||||
|
assert token in source
|
||||||
assert "if (entry.type !== 'manual') return false;" in source
|
assert "if (entry.type !== 'manual') return false;" in source
|
||||||
assert "type: 'manual'" in source
|
assert "type: 'manual'" in source
|
||||||
assert "_remember(userOverrides" not in set_enabled
|
assert "_remember(userOverrides" not in set_enabled
|
||||||
@@ -91,7 +95,9 @@ def test_capability_runtime_overrides_do_not_mask_claims():
|
|||||||
assert "'audio-mix'" not in reserved
|
assert "'audio-mix'" not in reserved
|
||||||
assert "'audio-input'" not in reserved
|
assert "'audio-input'" not in reserved
|
||||||
assert "'playback'" not in reserved
|
assert "'playback'" not in reserved
|
||||||
|
assert "'jobs'" not in reserved
|
||||||
assert "playback:" in source
|
assert "playback:" in source
|
||||||
|
assert "jobs:" in source
|
||||||
assert "'backend.routes'" in reserved
|
assert "'backend.routes'" in reserved
|
||||||
assert "'backend.routes':" not in source
|
assert "'backend.routes':" not in source
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user