sh3-core 0.21.2 → 0.22.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/__test__/fixtures.js +1 -1
- package/dist/__test__/reset.js +1 -1
- package/dist/__test__/smoke.test.js +2 -2
- package/dist/actions/contextMenuModel.test.js +6 -3
- package/dist/actions/ctx-actions.svelte.test.js +9 -9
- package/dist/actions/dispatcher-v3.test.js +8 -0
- package/dist/actions/dispatcher.svelte.d.ts +1 -2
- package/dist/actions/dispatcher.svelte.js +6 -7
- package/dist/actions/dispatcher.test.js +9 -12
- package/dist/actions/listActionsFromEntries.test.js +1 -2
- package/dist/actions/listActive.test.js +2 -3
- package/dist/actions/menuBarModel.test.js +1 -7
- package/dist/actions/paletteModel.test.js +1 -3
- package/dist/actions/scope-helpers.test.js +4 -4
- package/dist/actions/shardContext.test.js +2 -2
- package/dist/actions/state.svelte.d.ts +12 -2
- package/dist/actions/state.svelte.js +15 -12
- package/dist/actions/state.test.js +4 -4
- package/dist/api.d.ts +3 -3
- package/dist/api.js +1 -1
- package/dist/app/admin/adminShard.svelte.js +1 -1
- package/dist/app/store/storeShard.svelte.js +10 -5
- package/dist/app-appearance/appearanceShard.svelte.js +1 -5
- package/dist/apps/lifecycle.js +65 -33
- package/dist/apps/lifecycle.test.js +198 -10
- package/dist/artifact.d.ts +2 -0
- package/dist/build.js +1 -1
- package/dist/conflicts/adapter-documents.js +1 -2
- package/dist/createShell.js +1 -1
- package/dist/documents/handle.d.ts +9 -4
- package/dist/documents/handle.js +69 -45
- package/dist/documents/handle.test.js +99 -27
- package/dist/documents/index.d.ts +1 -1
- package/dist/documents/types.d.ts +16 -20
- package/dist/host.d.ts +1 -1
- package/dist/host.js +9 -56
- package/dist/host.svelte.test.js +31 -63
- package/dist/layout/LayoutRenderer.svelte +1 -1
- package/dist/layout/SlotContainer.svelte +1 -0
- package/dist/layout/inspection.js +19 -14
- package/dist/layout/inspection.svelte.test.js +136 -1
- package/dist/layout/slotHostPool.svelte.d.ts +2 -1
- package/dist/layout/slotHostPool.svelte.js +6 -3
- package/dist/layout/slotHostPool.test.js +17 -0
- package/dist/layout/store.projectScope.test.js +76 -0
- package/dist/layout/store.svelte.d.ts +6 -0
- package/dist/layout/store.svelte.js +43 -13
- package/dist/layout/tree-walk.d.ts +8 -1
- package/dist/layout/tree-walk.js +11 -1
- package/dist/layout/tree-walk.test.js +53 -1
- package/dist/layout/types.d.ts +27 -0
- package/dist/layout/types.test.js +28 -0
- package/dist/layouts-shard/LayoutsSection.svelte +1 -1
- package/dist/layouts-shard/layoutsShard.svelte.js +2 -5
- package/dist/layouts-shard/layoutsShard.svelte.test.js +2 -2
- package/dist/overlays/FloatFrame.svelte +4 -1
- package/dist/overlays/float.d.ts +7 -1
- package/dist/overlays/float.js +4 -0
- package/dist/projects-shard/ProjectsSection.svelte +1 -5
- package/dist/projects-shard/projectsShard.svelte.js +1 -5
- package/dist/registry/installer.js +1 -1
- package/dist/registry/loader.d.ts +1 -1
- package/dist/registry/loader.js +3 -3
- package/dist/registry/permission-descriptions.test.js +2 -2
- package/dist/registry/register.js +1 -1
- package/dist/registry/register.test.js +1 -1
- package/dist/runtime/runVerb-shell.test.js +1 -1
- package/dist/runtime/runVerb.js +2 -2
- package/dist/runtime/runVerb.test.js +9 -9
- package/dist/sh3Api/headless.js +1 -1
- package/dist/sh3core-shard/sh3coreShard.svelte.js +1 -6
- package/dist/shards/ctx-fetch.test.js +9 -9
- package/dist/shards/lifecycle.svelte.d.ts +108 -0
- package/dist/shards/lifecycle.svelte.js +551 -0
- package/dist/shards/lifecycle.test.js +139 -0
- package/dist/shards/types.d.ts +56 -22
- package/dist/shell-shard/shellShard.svelte.js +11 -5
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
- package/dist/shards/activate-browse.test.js +0 -120
- package/dist/shards/activate-contributions.test.js +0 -141
- package/dist/shards/activate-error-isolation.test.js +0 -98
- package/dist/shards/activate-fields.svelte.test.d.ts +0 -1
- package/dist/shards/activate-fields.svelte.test.js +0 -121
- package/dist/shards/activate-on-key-revoked.test.d.ts +0 -1
- package/dist/shards/activate-on-key-revoked.test.js +0 -60
- package/dist/shards/activate-runtime.test.d.ts +0 -1
- package/dist/shards/activate-runtime.test.js +0 -299
- package/dist/shards/activate-scopeid.test.d.ts +0 -1
- package/dist/shards/activate-scopeid.test.js +0 -21
- package/dist/shards/activate.svelte.d.ts +0 -102
- package/dist/shards/activate.svelte.js +0 -403
- /package/dist/{shards/activate-browse.test.d.ts → actions/dispatcher-v3.test.d.ts} +0 -0
- /package/dist/{shards/activate-contributions.test.d.ts → layout/store.projectScope.test.d.ts} +0 -0
- /package/dist/shards/{activate-error-isolation.test.d.ts → lifecycle.test.d.ts} +0 -0
|
@@ -1,98 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, beforeEach } from 'vitest';
|
|
2
|
-
import { MemoryDocumentBackend } from '../documents/backends';
|
|
3
|
-
import { __setDocumentBackend, __setActiveScope } from '../documents/config';
|
|
4
|
-
import { registerShard, activateShard, registeredShards, activeShards, __resetShardRegistryForTest, erroredShards, } from './activate.svelte';
|
|
5
|
-
describe('erroredShards map', () => {
|
|
6
|
-
beforeEach(() => {
|
|
7
|
-
__resetShardRegistryForTest();
|
|
8
|
-
__setDocumentBackend(new MemoryDocumentBackend());
|
|
9
|
-
__setActiveScope('tenant-a');
|
|
10
|
-
});
|
|
11
|
-
it('is empty after reset', () => {
|
|
12
|
-
expect(erroredShards.size).toBe(0);
|
|
13
|
-
});
|
|
14
|
-
it('supports the Map read API used by callers', () => {
|
|
15
|
-
expect(typeof erroredShards.has).toBe('function');
|
|
16
|
-
expect(typeof erroredShards.get).toBe('function');
|
|
17
|
-
expect(erroredShards.has('anything')).toBe(false);
|
|
18
|
-
});
|
|
19
|
-
});
|
|
20
|
-
describe('activateShard — unwind on activation failure', () => {
|
|
21
|
-
beforeEach(() => {
|
|
22
|
-
__resetShardRegistryForTest();
|
|
23
|
-
__setDocumentBackend(new MemoryDocumentBackend());
|
|
24
|
-
__setActiveScope('tenant-a');
|
|
25
|
-
});
|
|
26
|
-
it('unwinds partial state and records the error when activate throws', async () => {
|
|
27
|
-
const shard = {
|
|
28
|
-
manifest: {
|
|
29
|
-
id: 'broken',
|
|
30
|
-
label: 'Broken',
|
|
31
|
-
version: '0.0.0',
|
|
32
|
-
views: [],
|
|
33
|
-
},
|
|
34
|
-
activate(ctx) {
|
|
35
|
-
ctx.registerView('broken:view', { mount: () => ({ unmount() { } }) });
|
|
36
|
-
throw new Error('dependency missing');
|
|
37
|
-
},
|
|
38
|
-
};
|
|
39
|
-
registerShard(shard);
|
|
40
|
-
await expect(activateShard('broken')).rejects.toThrow('dependency missing');
|
|
41
|
-
expect(activeShards.has('broken')).toBe(false);
|
|
42
|
-
expect(registeredShards.has('broken')).toBe(true);
|
|
43
|
-
const entry = erroredShards.get('broken');
|
|
44
|
-
expect(entry).toBeDefined();
|
|
45
|
-
expect(entry === null || entry === void 0 ? void 0 : entry.id).toBe('broken');
|
|
46
|
-
expect(entry === null || entry === void 0 ? void 0 : entry.phase).toBe('launch');
|
|
47
|
-
expect(entry === null || entry === void 0 ? void 0 : entry.error).toBeInstanceOf(Error);
|
|
48
|
-
expect(typeof (entry === null || entry === void 0 ? void 0 : entry.timestamp)).toBe('number');
|
|
49
|
-
const { getView } = await import('./registry');
|
|
50
|
-
expect(getView('broken:view')).toBeUndefined();
|
|
51
|
-
});
|
|
52
|
-
it('records phase "autostart" when called with that option', async () => {
|
|
53
|
-
var _a;
|
|
54
|
-
const shard = {
|
|
55
|
-
manifest: { id: 'broken-auto', label: 'B', version: '0.0.0', views: [] },
|
|
56
|
-
activate() {
|
|
57
|
-
throw new Error('no');
|
|
58
|
-
},
|
|
59
|
-
};
|
|
60
|
-
registerShard(shard);
|
|
61
|
-
await expect(activateShard('broken-auto', { phase: 'autostart' })).rejects.toThrow('no');
|
|
62
|
-
expect((_a = erroredShards.get('broken-auto')) === null || _a === void 0 ? void 0 : _a.phase).toBe('autostart');
|
|
63
|
-
});
|
|
64
|
-
it('clears the error entry when the shard is re-registered', async () => {
|
|
65
|
-
const broken = {
|
|
66
|
-
manifest: { id: 'reborn', label: 'R', version: '0.0.0', views: [] },
|
|
67
|
-
activate() {
|
|
68
|
-
throw new Error('first try');
|
|
69
|
-
},
|
|
70
|
-
};
|
|
71
|
-
registerShard(broken);
|
|
72
|
-
await expect(activateShard('reborn')).rejects.toThrow('first try');
|
|
73
|
-
expect(erroredShards.has('reborn')).toBe(true);
|
|
74
|
-
const fixed = {
|
|
75
|
-
manifest: { id: 'reborn', label: 'R', version: '0.0.1', views: [] },
|
|
76
|
-
activate() { },
|
|
77
|
-
};
|
|
78
|
-
registerShard(fixed);
|
|
79
|
-
expect(erroredShards.has('reborn')).toBe(false);
|
|
80
|
-
});
|
|
81
|
-
it('clears the error entry when activation eventually succeeds', async () => {
|
|
82
|
-
let shouldFail = true;
|
|
83
|
-
const shard = {
|
|
84
|
-
manifest: { id: 'flaky', label: 'F', version: '0.0.0', views: [] },
|
|
85
|
-
activate() {
|
|
86
|
-
if (shouldFail)
|
|
87
|
-
throw new Error('first try');
|
|
88
|
-
},
|
|
89
|
-
};
|
|
90
|
-
registerShard(shard);
|
|
91
|
-
await expect(activateShard('flaky')).rejects.toThrow('first try');
|
|
92
|
-
expect(erroredShards.has('flaky')).toBe(true);
|
|
93
|
-
shouldFail = false;
|
|
94
|
-
await activateShard('flaky');
|
|
95
|
-
expect(erroredShards.has('flaky')).toBe(false);
|
|
96
|
-
expect(activeShards.has('flaky')).toBe(true);
|
|
97
|
-
});
|
|
98
|
-
});
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1,121 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
|
2
|
-
import { registerShard, activateShard, __resetShardRegistryForTest, } from './activate.svelte';
|
|
3
|
-
import { __resetViewRegistryForTest } from './registry';
|
|
4
|
-
import { __resetContributionsForTest } from '../contributions/registry';
|
|
5
|
-
import { __resetDecorationLayerForTest } from '../fields/decoration';
|
|
6
|
-
const makeShard = (overrides) => (Object.assign({ activate: () => { } }, overrides));
|
|
7
|
-
describe('ctx.sh3.fields integration', () => {
|
|
8
|
-
beforeEach(() => {
|
|
9
|
-
__resetShardRegistryForTest();
|
|
10
|
-
__resetViewRegistryForTest();
|
|
11
|
-
__resetContributionsForTest();
|
|
12
|
-
__resetDecorationLayerForTest();
|
|
13
|
-
document.body.innerHTML = '';
|
|
14
|
-
});
|
|
15
|
-
it('shard-scoped field is visible to a consumer via ctx.sh3.fields.list', async () => {
|
|
16
|
-
let value = 'dark';
|
|
17
|
-
registerShard(makeShard({
|
|
18
|
-
manifest: { id: 'producer', label: 'P', version: '0.0.0', views: [] },
|
|
19
|
-
activate(ctx) {
|
|
20
|
-
ctx.contributions.register('sh3.controllable-field', {
|
|
21
|
-
shape: 'imperative',
|
|
22
|
-
fieldId: 'theme',
|
|
23
|
-
label: 'Theme',
|
|
24
|
-
kind: 'enum',
|
|
25
|
-
enumValues: ['light', 'dark'],
|
|
26
|
-
get: () => value,
|
|
27
|
-
set: (v) => { value = v; },
|
|
28
|
-
});
|
|
29
|
-
},
|
|
30
|
-
}));
|
|
31
|
-
let consumerCtx = null;
|
|
32
|
-
registerShard(makeShard({
|
|
33
|
-
manifest: { id: 'consumer', label: 'C', version: '0.0.0', views: [] },
|
|
34
|
-
activate(ctx) { consumerCtx = ctx; },
|
|
35
|
-
}));
|
|
36
|
-
await activateShard('producer');
|
|
37
|
-
await activateShard('consumer');
|
|
38
|
-
const list = consumerCtx.sh3.fields.list();
|
|
39
|
-
expect(list).toHaveLength(1);
|
|
40
|
-
expect(list[0]).toMatchObject({
|
|
41
|
-
shardId: 'producer',
|
|
42
|
-
fieldId: 'theme',
|
|
43
|
-
kind: 'enum',
|
|
44
|
-
readonly: false,
|
|
45
|
-
source: 'contributed',
|
|
46
|
-
});
|
|
47
|
-
expect(consumerCtx.sh3.fields.get({ shardId: 'producer', fieldId: 'theme' })).toBe('dark');
|
|
48
|
-
await consumerCtx.sh3.fields.set({ shardId: 'producer', fieldId: 'theme' }, 'light');
|
|
49
|
-
expect(value).toBe('light');
|
|
50
|
-
});
|
|
51
|
-
it('contenteditable-style imperative+element pattern works end-to-end (set, decorate, unmount)', async () => {
|
|
52
|
-
const editorEl = document.createElement('div');
|
|
53
|
-
editorEl.contentEditable = 'true';
|
|
54
|
-
editorEl.textContent = 'initial';
|
|
55
|
-
document.body.appendChild(editorEl);
|
|
56
|
-
editorEl.getBoundingClientRect = () => ({ x: 5, y: 5, width: 100, height: 50, top: 5, left: 5, right: 105, bottom: 55 });
|
|
57
|
-
const set = vi.fn().mockImplementation((v) => { editorEl.textContent = String(v); });
|
|
58
|
-
registerShard(makeShard({
|
|
59
|
-
manifest: { id: 'producer', label: 'P', version: '0.0.0', views: [] },
|
|
60
|
-
activate(ctx) {
|
|
61
|
-
ctx.contributions.register('sh3.controllable-field', {
|
|
62
|
-
shape: 'imperative',
|
|
63
|
-
fieldId: 'body',
|
|
64
|
-
label: 'Body',
|
|
65
|
-
kind: 'string',
|
|
66
|
-
get: () => { var _a; return (_a = editorEl.textContent) !== null && _a !== void 0 ? _a : ''; },
|
|
67
|
-
set,
|
|
68
|
-
element: editorEl,
|
|
69
|
-
});
|
|
70
|
-
},
|
|
71
|
-
}));
|
|
72
|
-
let consumerCtx = null;
|
|
73
|
-
registerShard(makeShard({
|
|
74
|
-
manifest: { id: 'consumer', label: 'C', version: '0.0.0', views: [] },
|
|
75
|
-
activate(ctx) { consumerCtx = ctx; },
|
|
76
|
-
}));
|
|
77
|
-
await activateShard('producer');
|
|
78
|
-
await activateShard('consumer');
|
|
79
|
-
await consumerCtx.sh3.fields.set({ shardId: 'producer', fieldId: 'body' }, 'hello');
|
|
80
|
-
expect(set).toHaveBeenCalledWith('hello');
|
|
81
|
-
expect(editorEl.textContent).toBe('hello');
|
|
82
|
-
const off = consumerCtx.sh3.fields.attachDecoration({ shardId: 'producer', fieldId: 'body' }, () => {
|
|
83
|
-
const badge = document.createElement('span');
|
|
84
|
-
badge.dataset.kind = 'sh3-ai-badge';
|
|
85
|
-
return badge;
|
|
86
|
-
});
|
|
87
|
-
expect(document.querySelector('[data-kind="sh3-ai-badge"]')).not.toBeNull();
|
|
88
|
-
off();
|
|
89
|
-
expect(document.querySelector('[data-kind="sh3-ai-badge"]')).toBeNull();
|
|
90
|
-
});
|
|
91
|
-
it('listFields filters by shardId', async () => {
|
|
92
|
-
registerShard(makeShard({
|
|
93
|
-
manifest: { id: 'a', label: 'A', version: '0.0.0', views: [] },
|
|
94
|
-
activate(ctx) {
|
|
95
|
-
ctx.contributions.register('sh3.controllable-field', {
|
|
96
|
-
shape: 'imperative', fieldId: 'x', label: 'X', kind: 'string',
|
|
97
|
-
get: () => '1',
|
|
98
|
-
});
|
|
99
|
-
},
|
|
100
|
-
}));
|
|
101
|
-
registerShard(makeShard({
|
|
102
|
-
manifest: { id: 'b', label: 'B', version: '0.0.0', views: [] },
|
|
103
|
-
activate(ctx) {
|
|
104
|
-
ctx.contributions.register('sh3.controllable-field', {
|
|
105
|
-
shape: 'imperative', fieldId: 'y', label: 'Y', kind: 'string',
|
|
106
|
-
get: () => '2',
|
|
107
|
-
});
|
|
108
|
-
},
|
|
109
|
-
}));
|
|
110
|
-
let consumerCtx = null;
|
|
111
|
-
registerShard(makeShard({
|
|
112
|
-
manifest: { id: 'c', label: 'C', version: '0.0.0', views: [] },
|
|
113
|
-
activate(ctx) { consumerCtx = ctx; },
|
|
114
|
-
}));
|
|
115
|
-
await activateShard('a');
|
|
116
|
-
await activateShard('b');
|
|
117
|
-
await activateShard('c');
|
|
118
|
-
expect(consumerCtx.sh3.fields.list({ shardId: 'a' }).map((f) => f.fieldId)).toEqual(['x']);
|
|
119
|
-
expect(consumerCtx.sh3.fields.list({ shardId: 'b' }).map((f) => f.fieldId)).toEqual(['y']);
|
|
120
|
-
});
|
|
121
|
-
});
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1,60 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, beforeEach } from 'vitest';
|
|
2
|
-
import { MemoryDocumentBackend } from '../documents/backends';
|
|
3
|
-
import { __setDocumentBackend, __setActiveScope } from '../documents/config';
|
|
4
|
-
import { registerShard, activateShard, deactivateShard, __resetShardRegistryForTest } from './activate.svelte';
|
|
5
|
-
import { emit } from '../keys/revocation-bus.svelte';
|
|
6
|
-
describe('onKeyRevoked hook wiring', () => {
|
|
7
|
-
beforeEach(() => {
|
|
8
|
-
__resetShardRegistryForTest();
|
|
9
|
-
__setDocumentBackend(new MemoryDocumentBackend());
|
|
10
|
-
__setActiveScope('tenant-a');
|
|
11
|
-
});
|
|
12
|
-
it('fires onKeyRevoked when the bus emits for the shard', async () => {
|
|
13
|
-
const received = [];
|
|
14
|
-
registerShard({
|
|
15
|
-
manifest: { id: 'hook-shard', label: 'h', version: '0.0.0', views: [] },
|
|
16
|
-
activate() { },
|
|
17
|
-
onKeyRevoked(id) { received.push(id); },
|
|
18
|
-
});
|
|
19
|
-
await activateShard('hook-shard');
|
|
20
|
-
emit('hook-shard', 'key-abc');
|
|
21
|
-
// Handler is async; give microtasks a chance to flush.
|
|
22
|
-
await Promise.resolve();
|
|
23
|
-
expect(received).toEqual(['key-abc']);
|
|
24
|
-
});
|
|
25
|
-
it('does not fire for a different shardId', async () => {
|
|
26
|
-
const received = [];
|
|
27
|
-
registerShard({
|
|
28
|
-
manifest: { id: 'shard-x', label: 'x', version: '0.0.0', views: [] },
|
|
29
|
-
activate() { },
|
|
30
|
-
onKeyRevoked(id) { received.push(id); },
|
|
31
|
-
});
|
|
32
|
-
await activateShard('shard-x');
|
|
33
|
-
emit('shard-y', 'key-other');
|
|
34
|
-
await Promise.resolve();
|
|
35
|
-
expect(received).toHaveLength(0);
|
|
36
|
-
});
|
|
37
|
-
it('does not fire after deactivation', async () => {
|
|
38
|
-
const received = [];
|
|
39
|
-
registerShard({
|
|
40
|
-
manifest: { id: 'shard-deact', label: 'd', version: '0.0.0', views: [] },
|
|
41
|
-
activate() { },
|
|
42
|
-
onKeyRevoked(id) { received.push(id); },
|
|
43
|
-
});
|
|
44
|
-
await activateShard('shard-deact');
|
|
45
|
-
deactivateShard('shard-deact');
|
|
46
|
-
emit('shard-deact', 'key-gone');
|
|
47
|
-
await Promise.resolve();
|
|
48
|
-
expect(received).toHaveLength(0);
|
|
49
|
-
});
|
|
50
|
-
it('does not subscribe when onKeyRevoked is absent', async () => {
|
|
51
|
-
// Should not throw — just silently skips subscribing.
|
|
52
|
-
registerShard({
|
|
53
|
-
manifest: { id: 'no-hook', label: 'n', version: '0.0.0', views: [] },
|
|
54
|
-
activate() { },
|
|
55
|
-
});
|
|
56
|
-
await expect(activateShard('no-hook')).resolves.toBeUndefined();
|
|
57
|
-
// Emitting for a shard with no listener is a no-op.
|
|
58
|
-
expect(() => emit('no-hook', 'k')).not.toThrow();
|
|
59
|
-
});
|
|
60
|
-
});
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1,299 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
|
2
|
-
import { MemoryDocumentBackend } from '../documents/backends';
|
|
3
|
-
import { __setDocumentBackend, __setActiveScope } from '../documents/config';
|
|
4
|
-
import { registerShard, activateShard, __resetShardRegistryForTest, } from './activate.svelte';
|
|
5
|
-
import { __resetViewRegistryForTest } from './registry';
|
|
6
|
-
function programmaticVerb(name, summary, body) {
|
|
7
|
-
return {
|
|
8
|
-
name,
|
|
9
|
-
summary,
|
|
10
|
-
programmatic: true,
|
|
11
|
-
async run(vctx, args) {
|
|
12
|
-
if (body)
|
|
13
|
-
await body(vctx, args);
|
|
14
|
-
},
|
|
15
|
-
};
|
|
16
|
-
}
|
|
17
|
-
function plainVerb(name, summary) {
|
|
18
|
-
return { name, summary, async run() { } };
|
|
19
|
-
}
|
|
20
|
-
describe('ctx.listVerbs / ctx.runVerb (integration)', () => {
|
|
21
|
-
beforeEach(() => {
|
|
22
|
-
__resetShardRegistryForTest();
|
|
23
|
-
__resetViewRegistryForTest();
|
|
24
|
-
__setDocumentBackend(new MemoryDocumentBackend());
|
|
25
|
-
__setActiveScope('tenant-test');
|
|
26
|
-
});
|
|
27
|
-
it('listVerbs returns every verb across active shards with shardId', async () => {
|
|
28
|
-
registerShard({
|
|
29
|
-
manifest: { id: 'host', label: 'Host', version: '0.0.0', views: [] },
|
|
30
|
-
activate(ctx) {
|
|
31
|
-
ctx.registerVerb(programmaticVerb('a', 'first verb'));
|
|
32
|
-
ctx.registerVerb(plainVerb('b', 'second verb'));
|
|
33
|
-
},
|
|
34
|
-
});
|
|
35
|
-
let consumerCtx = null;
|
|
36
|
-
registerShard({
|
|
37
|
-
manifest: { id: 'consumer', label: 'C', version: '0.0.0', views: [] },
|
|
38
|
-
activate(ctx) {
|
|
39
|
-
consumerCtx = ctx;
|
|
40
|
-
},
|
|
41
|
-
});
|
|
42
|
-
await activateShard('host');
|
|
43
|
-
await activateShard('consumer');
|
|
44
|
-
const list = consumerCtx.sh3.listVerbs();
|
|
45
|
-
const a = list.find((v) => v.name === 'host:a');
|
|
46
|
-
const b = list.find((v) => v.name === 'host:b');
|
|
47
|
-
expect(a).toEqual({ shardId: 'host', name: 'host:a', summary: 'first verb', programmatic: true, schema: undefined });
|
|
48
|
-
expect(b).toEqual({ shardId: 'host', name: 'host:b', summary: 'second verb', programmatic: undefined, schema: undefined });
|
|
49
|
-
});
|
|
50
|
-
it('listVerbs({ programmaticOnly: true }) returns only verbs that opted in', async () => {
|
|
51
|
-
registerShard({
|
|
52
|
-
manifest: { id: 'host', label: 'Host', version: '0.0.0', views: [] },
|
|
53
|
-
activate(ctx) {
|
|
54
|
-
ctx.registerVerb(programmaticVerb('a', 'first verb'));
|
|
55
|
-
ctx.registerVerb(plainVerb('b', 'second verb'));
|
|
56
|
-
},
|
|
57
|
-
});
|
|
58
|
-
let consumerCtx = null;
|
|
59
|
-
registerShard({
|
|
60
|
-
manifest: { id: 'consumer', label: 'C', version: '0.0.0', views: [] },
|
|
61
|
-
activate(ctx) {
|
|
62
|
-
consumerCtx = ctx;
|
|
63
|
-
},
|
|
64
|
-
});
|
|
65
|
-
await activateShard('host');
|
|
66
|
-
await activateShard('consumer');
|
|
67
|
-
const list = consumerCtx.sh3.listVerbs({ programmaticOnly: true });
|
|
68
|
-
expect(list.find((v) => v.name === 'host:a')).toBeDefined();
|
|
69
|
-
expect(list.find((v) => v.name === 'host:b')).toBeUndefined();
|
|
70
|
-
expect(list.every((v) => v.programmatic === true)).toBe(true);
|
|
71
|
-
});
|
|
72
|
-
it('runVerb dispatches a programmatic verb and returns captured scrollback', async () => {
|
|
73
|
-
registerShard({
|
|
74
|
-
manifest: { id: 'host', label: 'Host', version: '0.0.0', views: [] },
|
|
75
|
-
activate(ctx) {
|
|
76
|
-
ctx.registerVerb(programmaticVerb('echo', 'echoes args', async (vctx, args) => {
|
|
77
|
-
vctx.scrollback.push({ kind: 'status', text: `echo: ${args.join(' ')}`, level: 'info', ts: 0 });
|
|
78
|
-
}));
|
|
79
|
-
},
|
|
80
|
-
});
|
|
81
|
-
let consumerCtx = null;
|
|
82
|
-
registerShard({
|
|
83
|
-
manifest: { id: 'consumer', label: 'C', version: '0.0.0', views: [] },
|
|
84
|
-
activate(ctx) {
|
|
85
|
-
consumerCtx = ctx;
|
|
86
|
-
},
|
|
87
|
-
});
|
|
88
|
-
await activateShard('host');
|
|
89
|
-
await activateShard('consumer');
|
|
90
|
-
const out = await consumerCtx.sh3.runVerb('host', 'host:echo', ['hello']);
|
|
91
|
-
expect(out.scrollback).toHaveLength(1);
|
|
92
|
-
expect(out.scrollback[0]).toMatchObject({ kind: 'status', text: 'echo: hello' });
|
|
93
|
-
});
|
|
94
|
-
it('runVerb rejects when the target verb is not programmatic', async () => {
|
|
95
|
-
registerShard({
|
|
96
|
-
manifest: { id: 'host', label: 'Host', version: '0.0.0', views: [] },
|
|
97
|
-
activate(ctx) {
|
|
98
|
-
ctx.registerVerb(plainVerb('plain', 'no opt-in'));
|
|
99
|
-
},
|
|
100
|
-
});
|
|
101
|
-
let consumerCtx = null;
|
|
102
|
-
registerShard({
|
|
103
|
-
manifest: { id: 'consumer', label: 'C', version: '0.0.0', views: [] },
|
|
104
|
-
activate(ctx) {
|
|
105
|
-
consumerCtx = ctx;
|
|
106
|
-
},
|
|
107
|
-
});
|
|
108
|
-
await activateShard('host');
|
|
109
|
-
await activateShard('consumer');
|
|
110
|
-
await expect(consumerCtx.sh3.runVerb('host', 'host:plain', [])).rejects.toThrow('verb "host:plain" is not programmatic');
|
|
111
|
-
});
|
|
112
|
-
it('runVerb populates ctx.structuredArgs when opts.structured is set', async () => {
|
|
113
|
-
let observed = undefined;
|
|
114
|
-
registerShard({
|
|
115
|
-
manifest: { id: 'host', label: 'Host', version: '0.0.0', views: [] },
|
|
116
|
-
activate(ctx) {
|
|
117
|
-
ctx.registerVerb(programmaticVerb('schemaCheck', 'reads structuredArgs', async (vctx) => {
|
|
118
|
-
observed = vctx.structuredArgs;
|
|
119
|
-
}));
|
|
120
|
-
},
|
|
121
|
-
});
|
|
122
|
-
let consumerCtx = null;
|
|
123
|
-
registerShard({
|
|
124
|
-
manifest: { id: 'consumer', label: 'C', version: '0.0.0', views: [] },
|
|
125
|
-
activate(ctx) {
|
|
126
|
-
consumerCtx = ctx;
|
|
127
|
-
},
|
|
128
|
-
});
|
|
129
|
-
await activateShard('host');
|
|
130
|
-
await activateShard('consumer');
|
|
131
|
-
await consumerCtx.sh3.runVerb('host', 'host:schemaCheck', [], { structured: { foo: 'bar' } });
|
|
132
|
-
expect(observed).toEqual({ foo: 'bar' });
|
|
133
|
-
});
|
|
134
|
-
it('runVerb rejects on unknown shardId', async () => {
|
|
135
|
-
let consumerCtx = null;
|
|
136
|
-
registerShard({
|
|
137
|
-
manifest: { id: 'consumer', label: 'C', version: '0.0.0', views: [] },
|
|
138
|
-
activate(ctx) {
|
|
139
|
-
consumerCtx = ctx;
|
|
140
|
-
},
|
|
141
|
-
});
|
|
142
|
-
await activateShard('consumer');
|
|
143
|
-
await expect(consumerCtx.sh3.runVerb('missing', 'x', [])).rejects.toThrow('unknown shard: missing');
|
|
144
|
-
});
|
|
145
|
-
it('runVerb rejects on unknown verb', async () => {
|
|
146
|
-
registerShard({
|
|
147
|
-
manifest: { id: 'host', label: 'Host', version: '0.0.0', views: [] },
|
|
148
|
-
activate(ctx) {
|
|
149
|
-
ctx.registerVerb(programmaticVerb('present', 'exists'));
|
|
150
|
-
},
|
|
151
|
-
});
|
|
152
|
-
let consumerCtx = null;
|
|
153
|
-
registerShard({
|
|
154
|
-
manifest: { id: 'consumer', label: 'C', version: '0.0.0', views: [] },
|
|
155
|
-
activate(ctx) {
|
|
156
|
-
consumerCtx = ctx;
|
|
157
|
-
},
|
|
158
|
-
});
|
|
159
|
-
await activateShard('host');
|
|
160
|
-
await activateShard('consumer');
|
|
161
|
-
await expect(consumerCtx.sh3.runVerb('host', 'host:absent', [])).rejects.toThrow('unknown verb: host:absent');
|
|
162
|
-
});
|
|
163
|
-
it('verbs declaring schema.input expose it via listVerbs', async () => {
|
|
164
|
-
registerShard({
|
|
165
|
-
manifest: { id: 'host', label: 'Host', version: '0.0.0', views: [] },
|
|
166
|
-
activate(ctx) {
|
|
167
|
-
ctx.registerVerb({
|
|
168
|
-
name: 'typed',
|
|
169
|
-
summary: 'has schema',
|
|
170
|
-
programmatic: true,
|
|
171
|
-
schema: {
|
|
172
|
-
input: {
|
|
173
|
-
type: 'object',
|
|
174
|
-
properties: { msg: { type: 'string', description: 'message' } },
|
|
175
|
-
required: ['msg'],
|
|
176
|
-
},
|
|
177
|
-
},
|
|
178
|
-
async run() { },
|
|
179
|
-
});
|
|
180
|
-
},
|
|
181
|
-
});
|
|
182
|
-
let consumerCtx = null;
|
|
183
|
-
registerShard({
|
|
184
|
-
manifest: { id: 'consumer', label: 'C', version: '0.0.0', views: [] },
|
|
185
|
-
activate(ctx) {
|
|
186
|
-
consumerCtx = ctx;
|
|
187
|
-
},
|
|
188
|
-
});
|
|
189
|
-
await activateShard('host');
|
|
190
|
-
await activateShard('consumer');
|
|
191
|
-
const list = consumerCtx.sh3.listVerbs();
|
|
192
|
-
const typed = list.find((v) => v.name === 'host:typed');
|
|
193
|
-
expect(typed === null || typed === void 0 ? void 0 : typed.schema).toEqual({
|
|
194
|
-
input: {
|
|
195
|
-
type: 'object',
|
|
196
|
-
properties: { msg: { type: 'string', description: 'message' } },
|
|
197
|
-
required: ['msg'],
|
|
198
|
-
},
|
|
199
|
-
});
|
|
200
|
-
});
|
|
201
|
-
it('verbNamespace overrides the shard-id prefix', async () => {
|
|
202
|
-
registerShard({
|
|
203
|
-
manifest: {
|
|
204
|
-
id: 'sh3-dirt-viewer',
|
|
205
|
-
label: 'Dirt',
|
|
206
|
-
version: '0.0.0',
|
|
207
|
-
views: [],
|
|
208
|
-
verbNamespace: 'dirt',
|
|
209
|
-
},
|
|
210
|
-
activate(ctx) {
|
|
211
|
-
ctx.registerVerb(plainVerb('publish', 'publish runtime'));
|
|
212
|
-
},
|
|
213
|
-
});
|
|
214
|
-
let consumerCtx = null;
|
|
215
|
-
registerShard({
|
|
216
|
-
manifest: { id: 'consumer', label: 'C', version: '0.0.0', views: [] },
|
|
217
|
-
activate(ctx) { consumerCtx = ctx; },
|
|
218
|
-
});
|
|
219
|
-
await activateShard('sh3-dirt-viewer');
|
|
220
|
-
await activateShard('consumer');
|
|
221
|
-
const names = consumerCtx.sh3.listVerbs().map((v) => v.name);
|
|
222
|
-
expect(names).toContain('dirt:publish');
|
|
223
|
-
expect(names.find((n) => n.startsWith('sh3-dirt-viewer:'))).toBeUndefined();
|
|
224
|
-
});
|
|
225
|
-
it('verbNamespace collision across shards: first wins, second warns', async () => {
|
|
226
|
-
var _a;
|
|
227
|
-
registerShard({
|
|
228
|
-
manifest: {
|
|
229
|
-
id: 'shard-a',
|
|
230
|
-
label: 'A',
|
|
231
|
-
version: '0.0.0',
|
|
232
|
-
views: [],
|
|
233
|
-
verbNamespace: 'shared',
|
|
234
|
-
},
|
|
235
|
-
activate(ctx) {
|
|
236
|
-
ctx.registerVerb(plainVerb('go', 'A go'));
|
|
237
|
-
},
|
|
238
|
-
});
|
|
239
|
-
registerShard({
|
|
240
|
-
manifest: {
|
|
241
|
-
id: 'shard-b',
|
|
242
|
-
label: 'B',
|
|
243
|
-
version: '0.0.0',
|
|
244
|
-
views: [],
|
|
245
|
-
verbNamespace: 'shared',
|
|
246
|
-
},
|
|
247
|
-
activate(ctx) {
|
|
248
|
-
// Collides with shard-a's 'shared:go'; should be skipped.
|
|
249
|
-
ctx.registerVerb(plainVerb('go', 'B go'));
|
|
250
|
-
// Distinct name in same namespace should still register.
|
|
251
|
-
ctx.registerVerb(plainVerb('only-b', 'B-only verb'));
|
|
252
|
-
},
|
|
253
|
-
});
|
|
254
|
-
let consumerCtx = null;
|
|
255
|
-
registerShard({
|
|
256
|
-
manifest: { id: 'consumer', label: 'C', version: '0.0.0', views: [] },
|
|
257
|
-
activate(ctx) { consumerCtx = ctx; },
|
|
258
|
-
});
|
|
259
|
-
await activateShard('shard-a');
|
|
260
|
-
const warn = vi.spyOn(console, 'warn').mockImplementation(() => { });
|
|
261
|
-
await activateShard('shard-b');
|
|
262
|
-
await activateShard('consumer');
|
|
263
|
-
expect(warn).toHaveBeenCalled();
|
|
264
|
-
expect(warn.mock.calls.some((c) => /shared:go/.test(String(c[0])))).toBe(true);
|
|
265
|
-
warn.mockRestore();
|
|
266
|
-
const list = consumerCtx.sh3.listVerbs();
|
|
267
|
-
const shared = list.find((v) => v.name === 'shared:go');
|
|
268
|
-
// The losing registration must not have been tracked; deactivating
|
|
269
|
-
// shard-b later (covered elsewhere) would otherwise remove A's verb.
|
|
270
|
-
expect(shared === null || shared === void 0 ? void 0 : shared.summary).toBe('A go');
|
|
271
|
-
expect(shared === null || shared === void 0 ? void 0 : shared.shardId).toBe('shard-a');
|
|
272
|
-
expect((_a = list.find((v) => v.name === 'shared:only-b')) === null || _a === void 0 ? void 0 : _a.shardId).toBe('shard-b');
|
|
273
|
-
});
|
|
274
|
-
it('shell shard registers bare names regardless of verbNamespace setting', async () => {
|
|
275
|
-
registerShard({
|
|
276
|
-
manifest: {
|
|
277
|
-
id: 'shell',
|
|
278
|
-
label: 'Shell',
|
|
279
|
-
version: '0.0.0',
|
|
280
|
-
views: [],
|
|
281
|
-
// Intentionally set — should be ignored for the shell shard.
|
|
282
|
-
verbNamespace: 'ignored',
|
|
283
|
-
},
|
|
284
|
-
activate(ctx) {
|
|
285
|
-
ctx.registerVerb(plainVerb('clear', 'clear scrollback'));
|
|
286
|
-
},
|
|
287
|
-
});
|
|
288
|
-
let consumerCtx = null;
|
|
289
|
-
registerShard({
|
|
290
|
-
manifest: { id: 'consumer', label: 'C', version: '0.0.0', views: [] },
|
|
291
|
-
activate(ctx) { consumerCtx = ctx; },
|
|
292
|
-
});
|
|
293
|
-
await activateShard('shell');
|
|
294
|
-
await activateShard('consumer');
|
|
295
|
-
const names = consumerCtx.sh3.listVerbs().map((v) => v.name);
|
|
296
|
-
expect(names).toContain('clear');
|
|
297
|
-
expect(names.find((n) => n.startsWith('ignored:'))).toBeUndefined();
|
|
298
|
-
});
|
|
299
|
-
});
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, beforeEach } from 'vitest';
|
|
2
|
-
import { MemoryDocumentBackend } from '../documents/backends';
|
|
3
|
-
import { __setDocumentBackend, __setActiveScope } from '../documents/config';
|
|
4
|
-
import { registerShard, activateShard, __resetShardRegistryForTest } from './activate.svelte';
|
|
5
|
-
describe('ctx.tenantId reflects active scope', () => {
|
|
6
|
-
beforeEach(() => {
|
|
7
|
-
__resetShardRegistryForTest();
|
|
8
|
-
__setDocumentBackend(new MemoryDocumentBackend());
|
|
9
|
-
__setActiveScope('scope-a');
|
|
10
|
-
});
|
|
11
|
-
it('is present unconditionally on ctx', async () => {
|
|
12
|
-
let captured = null;
|
|
13
|
-
const shard = {
|
|
14
|
-
manifest: { id: 'test-scopeid', label: 't', version: '0.0.0', views: [] },
|
|
15
|
-
activate(ctx) { captured = ctx; },
|
|
16
|
-
};
|
|
17
|
-
registerShard(shard);
|
|
18
|
-
await activateShard('test-scopeid');
|
|
19
|
-
expect(captured.tenantId).toBe('scope-a');
|
|
20
|
-
});
|
|
21
|
-
});
|