mirror of
https://github.com/got-feedBack/feedBack.git
synced 2026-09-12 19:58:31 +00:00
Implement
This commit is contained in:
@@ -50,7 +50,7 @@ test('diagnostics export expected compatibility shim surfaces', () => {
|
||||
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 api = window.slopsmith.capabilities;
|
||||
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');
|
||||
assert.ok(playback, 'playback should be part of the active runtime graph when participants register');
|
||||
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)
|
||||
// left this list when their domains were promoted.
|
||||
const futureDomains = [
|
||||
'ui.navigation', 'ui.plugin-screens', 'settings',
|
||||
'backend.routes', 'ui.player-controls',
|
||||
'ui.player-panels', 'ui.player-overlays', 'plugins', 'jobs', 'midi-control',
|
||||
'tempo-clock',
|
||||
'ui.navigation', 'ui.plugin-screens', 'settings', 'backend.routes', 'ui.player-controls', 'ui.player-panels', 'ui.player-overlays', 'plugins', 'midi-control', 'tempo-clock',
|
||||
];
|
||||
|
||||
for (const domain of futureDomains) {
|
||||
|
||||
@@ -65,6 +65,9 @@ function loadInspector(snapshot, options = {}) {
|
||||
playback: options.playbackSnapshot ? {
|
||||
snapshot: () => options.playbackSnapshot,
|
||||
} : undefined,
|
||||
jobs: options.jobsSnapshot ? {
|
||||
snapshot: () => options.jobsSnapshot,
|
||||
} : undefined,
|
||||
},
|
||||
navigator: { clipboard: { writeText: async () => {} } },
|
||||
document: {
|
||||
@@ -114,6 +117,41 @@ test('capability inspector renders playback session route loop bridges and outco
|
||||
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', () => {
|
||||
const snapshot = {
|
||||
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,
|
||||
};
|
||||
Reference in New Issue
Block a user