@fias/arche-sdk 2.30.0 → 2.32.0
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/entity-ops.d.ts +10 -2
- package/dist/entity-ops.d.ts.map +1 -1
- package/dist/entity-ops.js +11 -3
- package/dist/entity-ops.js.map +1 -1
- package/dist/generated/permissions.d.ts +1 -1
- package/dist/generated/permissions.d.ts.map +1 -1
- package/dist/generated/permissions.js +6 -0
- package/dist/generated/permissions.js.map +1 -1
- package/dist/hooks.d.ts +61 -4
- package/dist/hooks.d.ts.map +1 -1
- package/dist/hooks.js +197 -18
- package/dist/hooks.js.map +1 -1
- package/dist/hooks.test.js +185 -1
- package/dist/hooks.test.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -3
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +162 -11
- package/dist/types.d.ts +279 -22
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/templates/default/AGENTS.md +142 -31
- package/templates/default/CLAUDE.md +142 -31
package/dist/hooks.test.js
CHANGED
|
@@ -299,6 +299,15 @@ describe('SDK hooks', () => {
|
|
|
299
299
|
chrome.openAssistant();
|
|
300
300
|
expect(mockBridge.openAssistant).toHaveBeenCalled();
|
|
301
301
|
});
|
|
302
|
+
it('carries where the person came from, null when nothing opened the arche', () => {
|
|
303
|
+
const { useFiasChrome } = require('./hooks');
|
|
304
|
+
expect(useFiasChrome().opener).toBeNull();
|
|
305
|
+
mockBridge.getChrome.mockReturnValue({
|
|
306
|
+
...HOST_STATE,
|
|
307
|
+
opener: { kind: 'forge', name: 'My Arche' },
|
|
308
|
+
});
|
|
309
|
+
expect(useFiasChrome().opener).toEqual({ kind: 'forge', name: 'My Arche' });
|
|
310
|
+
});
|
|
302
311
|
it('cleanup unsubscribes from chrome updates', () => {
|
|
303
312
|
const unsubscribe = jest.fn();
|
|
304
313
|
mockBridge.onChromeUpdate.mockReturnValue(unsubscribe);
|
|
@@ -854,6 +863,116 @@ describe('SDK hooks', () => {
|
|
|
854
863
|
expect(mockBridge.request).toHaveBeenCalledWith(...entityOp(entity_ops_1.PLUGIN_WORKSPACES_ENTITY_ID, 'create', { forActingHolder: true }));
|
|
855
864
|
});
|
|
856
865
|
});
|
|
866
|
+
describe('useFiasWorkspaces() member targets (ADR-040 §4)', () => {
|
|
867
|
+
it('updateMember / removeMember send a ref target as { ref }, a string as { username }', async () => {
|
|
868
|
+
const { useFiasWorkspaces } = require('./hooks');
|
|
869
|
+
mockBridge.request.mockResolvedValue(output({ member: {} }));
|
|
870
|
+
await useFiasWorkspaces().updateMember('ws_1', { ref: 'aur_a' }, 'admin');
|
|
871
|
+
expect(mockBridge.request).toHaveBeenLastCalledWith(...entityOp(entity_ops_1.PLUGIN_WORKSPACES_ENTITY_ID, 'update_member', {
|
|
872
|
+
workspaceId: 'ws_1',
|
|
873
|
+
ref: 'aur_a',
|
|
874
|
+
role: 'admin',
|
|
875
|
+
}));
|
|
876
|
+
mockBridge.request.mockResolvedValue(output({}));
|
|
877
|
+
await useFiasWorkspaces().removeMember('ws_1', 'alice');
|
|
878
|
+
expect(mockBridge.request).toHaveBeenLastCalledWith(...entityOp(entity_ops_1.PLUGIN_WORKSPACES_ENTITY_ID, 'remove_member', {
|
|
879
|
+
workspaceId: 'ws_1',
|
|
880
|
+
username: 'alice',
|
|
881
|
+
}));
|
|
882
|
+
});
|
|
883
|
+
});
|
|
884
|
+
describe('useFiasWorkspaces() member target guard (ADR-040 §4)', () => {
|
|
885
|
+
it('a missing target (m.username on a ref roster) is a clear error, not a TypeError', async () => {
|
|
886
|
+
const { useFiasWorkspaces } = require('./hooks');
|
|
887
|
+
await expect(useFiasWorkspaces().removeMember('ws_1', undefined)).rejects.toThrow(/name the member by \{ ref \}/);
|
|
888
|
+
expect(mockBridge.request).not.toHaveBeenCalled();
|
|
889
|
+
});
|
|
890
|
+
});
|
|
891
|
+
describe('useFiasWorkspaces().resolvePeople (ADR-040)', () => {
|
|
892
|
+
it('sends the refs and the workspace, and returns the names or nulls in order', async () => {
|
|
893
|
+
const { useFiasWorkspaces } = require('./hooks');
|
|
894
|
+
const people = [{ ref: 'aur_a', displayName: 'Ada', avatar: null }, null];
|
|
895
|
+
mockBridge.request.mockResolvedValue(output({ people }));
|
|
896
|
+
await expect(useFiasWorkspaces().resolvePeople(['aur_a', 'aur_b'], 'ws_1')).resolves.toEqual(people);
|
|
897
|
+
expect(mockBridge.request).toHaveBeenCalledWith(...entityOp(entity_ops_1.PLUGIN_WORKSPACES_ENTITY_ID, 'resolve_people', {
|
|
898
|
+
refs: ['aur_a', 'aur_b'],
|
|
899
|
+
workspaceId: 'ws_1',
|
|
900
|
+
}));
|
|
901
|
+
});
|
|
902
|
+
it('an empty list answers [] without a call', async () => {
|
|
903
|
+
const { useFiasWorkspaces } = require('./hooks');
|
|
904
|
+
await expect(useFiasWorkspaces().resolvePeople([], 'ws_1')).resolves.toEqual([]);
|
|
905
|
+
expect(mockBridge.request).not.toHaveBeenCalled();
|
|
906
|
+
});
|
|
907
|
+
it('pages more than 100 refs and keeps their order', async () => {
|
|
908
|
+
const { useFiasWorkspaces } = require('./hooks');
|
|
909
|
+
const refs = Array.from({ length: 150 }, (_, i) => `aur_${i}`);
|
|
910
|
+
mockBridge.request
|
|
911
|
+
.mockResolvedValueOnce(output({ people: refs.slice(0, 100).map(() => null) }))
|
|
912
|
+
.mockResolvedValueOnce(output({
|
|
913
|
+
people: refs.slice(100).map((ref) => ({ ref, displayName: ref, avatar: null })),
|
|
914
|
+
}));
|
|
915
|
+
const people = await useFiasWorkspaces().resolvePeople(refs, 'ws_1');
|
|
916
|
+
expect(mockBridge.request).toHaveBeenCalledTimes(2);
|
|
917
|
+
expect(people).toHaveLength(150);
|
|
918
|
+
expect(people[99]).toBeNull();
|
|
919
|
+
expect(people[149]).toMatchObject({ ref: 'aur_149' });
|
|
920
|
+
});
|
|
921
|
+
it('omits workspaceId when none is given (own ref only)', async () => {
|
|
922
|
+
const { useFiasWorkspaces } = require('./hooks');
|
|
923
|
+
mockBridge.request.mockResolvedValue(output({ people: [null] }));
|
|
924
|
+
await useFiasWorkspaces().resolvePeople(['aur_a']);
|
|
925
|
+
expect(mockBridge.request).toHaveBeenCalledWith(...entityOp(entity_ops_1.PLUGIN_WORKSPACES_ENTITY_ID, 'resolve_people', { refs: ['aur_a'] }));
|
|
926
|
+
});
|
|
927
|
+
});
|
|
928
|
+
describe('useFiasWorkspaces().addPeople (ADR-040 Q12)', () => {
|
|
929
|
+
it('opens the host sheet with the workspace and suggested role, and returns who was added', async () => {
|
|
930
|
+
const { useFiasWorkspaces } = require('./hooks');
|
|
931
|
+
const added = [{ ref: 'aur_abcdefghijklmnopqrst', displayName: 'Ada', role: 'viewer' }];
|
|
932
|
+
mockBridge.request.mockResolvedValue({ added });
|
|
933
|
+
await expect(useFiasWorkspaces().addPeople('ws_1', { role: 'viewer' })).resolves.toEqual({
|
|
934
|
+
added,
|
|
935
|
+
});
|
|
936
|
+
expect(mockBridge.request).toHaveBeenCalledWith('client_entity_invoke', {
|
|
937
|
+
entityId: 'ent_intg_workspace_add_people_00',
|
|
938
|
+
input: { workspaceId: 'ws_1', role: 'viewer' },
|
|
939
|
+
}, expect.any(Number));
|
|
940
|
+
});
|
|
941
|
+
it("the host's typed refusal reaches the app as its code", async () => {
|
|
942
|
+
const { useFiasWorkspaces } = require('./hooks');
|
|
943
|
+
mockBridge.request.mockResolvedValue({
|
|
944
|
+
error: { code: 'SHEET_BUSY', message: 'Another request is waiting' },
|
|
945
|
+
});
|
|
946
|
+
await expect(useFiasWorkspaces().addPeople('ws_1')).rejects.toMatchObject({
|
|
947
|
+
code: 'SHEET_BUSY',
|
|
948
|
+
});
|
|
949
|
+
});
|
|
950
|
+
it('a host without the sheet answers ADD_PEOPLE_UNAVAILABLE', async () => {
|
|
951
|
+
const { useFiasWorkspaces } = require('./hooks');
|
|
952
|
+
const { FiasBridgeError } = jest.requireActual('./bridge');
|
|
953
|
+
mockBridge.request.mockRejectedValue(new FiasBridgeError('client_entity_invoke: no client-side executor registered for ent_intg_workspace_add_people_00', 'BRIDGE_ERROR'));
|
|
954
|
+
await expect(useFiasWorkspaces().addPeople('ws_1')).rejects.toMatchObject({
|
|
955
|
+
code: 'ADD_PEOPLE_UNAVAILABLE',
|
|
956
|
+
});
|
|
957
|
+
});
|
|
958
|
+
it('recognises the host code CLIENT_ENTITY_UNAVAILABLE, whatever the message', async () => {
|
|
959
|
+
const { useFiasWorkspaces } = require('./hooks');
|
|
960
|
+
const { FiasBridgeError } = jest.requireActual('./bridge');
|
|
961
|
+
mockBridge.request.mockRejectedValue(new FiasBridgeError('Not here', 'CLIENT_ENTITY_UNAVAILABLE'));
|
|
962
|
+
await expect(useFiasWorkspaces().addPeople('ws_1')).rejects.toMatchObject({
|
|
963
|
+
code: 'ADD_PEOPLE_UNAVAILABLE',
|
|
964
|
+
});
|
|
965
|
+
});
|
|
966
|
+
it('passes any other bridge error through unchanged', async () => {
|
|
967
|
+
const { useFiasWorkspaces } = require('./hooks');
|
|
968
|
+
const { FiasBridgeError } = jest.requireActual('./bridge');
|
|
969
|
+
mockBridge.request.mockRejectedValue(new FiasBridgeError('Permission denied: data:workspace', 'BRIDGE_ERROR'));
|
|
970
|
+
await expect(useFiasWorkspaces().addPeople('ws_1')).rejects.toMatchObject({
|
|
971
|
+
code: 'BRIDGE_ERROR',
|
|
972
|
+
message: 'Permission denied: data:workspace',
|
|
973
|
+
});
|
|
974
|
+
});
|
|
975
|
+
});
|
|
857
976
|
// ADR-034 phase 0b. The real-render cases (stale answers across workspace
|
|
858
977
|
// switches, the render before effects run, refetch) live in
|
|
859
978
|
// use-fias-access.render.test.tsx, which CI runs through the package's
|
|
@@ -1015,7 +1134,7 @@ describe('SDK hooks', () => {
|
|
|
1015
1134
|
await expect(state.openAccessDialog()).rejects.toMatchObject({ code: 'NOT_IN_WORKSPACE' });
|
|
1016
1135
|
});
|
|
1017
1136
|
});
|
|
1018
|
-
describe('useCreativeLibrary().pick (ADR-
|
|
1137
|
+
describe('useCreativeLibrary().pick (ADR-037 D8)', () => {
|
|
1019
1138
|
const PICKED = {
|
|
1020
1139
|
item: { itemId: 'vci_' + 'a'.repeat(32), kind: 'style', name: 'Ink', version: 2 },
|
|
1021
1140
|
};
|
|
@@ -1053,6 +1172,71 @@ describe('SDK hooks', () => {
|
|
|
1053
1172
|
});
|
|
1054
1173
|
});
|
|
1055
1174
|
});
|
|
1175
|
+
describe('useOpener (ADR-037 D8)', () => {
|
|
1176
|
+
const FILE = 'doc_' + 'a'.repeat(32);
|
|
1177
|
+
const opener = { kind: 'forge', name: 'My Arche' };
|
|
1178
|
+
const CHROME = {
|
|
1179
|
+
focusMode: false,
|
|
1180
|
+
focusModeAvailable: false,
|
|
1181
|
+
sidebarAvailable: false,
|
|
1182
|
+
sidebarCollapsed: false,
|
|
1183
|
+
hostExpandedBar: true,
|
|
1184
|
+
hostExpandButton: true,
|
|
1185
|
+
opener,
|
|
1186
|
+
};
|
|
1187
|
+
it('names where the person came from, without taking the chrome over', () => {
|
|
1188
|
+
mockBridge.getChrome.mockReturnValue(CHROME);
|
|
1189
|
+
const { useOpener } = require('./hooks');
|
|
1190
|
+
expect(useOpener().opener).toEqual(opener);
|
|
1191
|
+
expect(mockBridge.setChrome).not.toHaveBeenCalled();
|
|
1192
|
+
});
|
|
1193
|
+
it('returns there only when there is an opener', () => {
|
|
1194
|
+
const { useOpener } = require('./hooks');
|
|
1195
|
+
useOpener().returnTo();
|
|
1196
|
+
expect(mockBridge.navigateHost).not.toHaveBeenCalled();
|
|
1197
|
+
mockBridge.getChrome.mockReturnValue(CHROME);
|
|
1198
|
+
useOpener().returnTo();
|
|
1199
|
+
expect(mockBridge.navigateHost).toHaveBeenCalledWith('opener');
|
|
1200
|
+
});
|
|
1201
|
+
it('offers a file through the host’s sheet, waiting on the person', async () => {
|
|
1202
|
+
mockBridge.getChrome.mockReturnValue(CHROME);
|
|
1203
|
+
mockBridge.request.mockResolvedValue({ sent: true, name: 'pumpkin.png' });
|
|
1204
|
+
const { useOpener } = require('./hooks');
|
|
1205
|
+
await expect(useOpener().send({ fileId: FILE, name: 'pumpkin.png' })).resolves.toEqual({
|
|
1206
|
+
sent: true,
|
|
1207
|
+
name: 'pumpkin.png',
|
|
1208
|
+
});
|
|
1209
|
+
const [op, payload, timeout] = mockBridge.request.mock.calls[0];
|
|
1210
|
+
expect(op).toBe('client_entity_invoke');
|
|
1211
|
+
expect(payload).toEqual({
|
|
1212
|
+
entityId: entity_ops_1.SEND_TO_OPENER_ENTITY_ID,
|
|
1213
|
+
input: { fileId: FILE, name: 'pumpkin.png' },
|
|
1214
|
+
});
|
|
1215
|
+
expect(timeout).toBeGreaterThanOrEqual(5 * 60 * 1000);
|
|
1216
|
+
});
|
|
1217
|
+
it('refuses with NO_OPENER when nothing opened this arche, asking nothing', async () => {
|
|
1218
|
+
const { useOpener } = require('./hooks');
|
|
1219
|
+
const api = useOpener();
|
|
1220
|
+
expect(api.opener).toBeNull();
|
|
1221
|
+
await expect(api.send({ fileId: FILE })).rejects.toMatchObject({ code: 'NO_OPENER' });
|
|
1222
|
+
expect(mockBridge.request).not.toHaveBeenCalled();
|
|
1223
|
+
});
|
|
1224
|
+
it('turns a refusal into a typed error, and a host without it into SEND_UNAVAILABLE', async () => {
|
|
1225
|
+
mockBridge.getChrome.mockReturnValue(CHROME);
|
|
1226
|
+
const { useOpener } = require('./hooks');
|
|
1227
|
+
mockBridge.request.mockResolvedValueOnce({
|
|
1228
|
+
error: { code: 'SHEET_BUSY', message: 'Another request is waiting' },
|
|
1229
|
+
});
|
|
1230
|
+
await expect(useOpener().send({ fileId: FILE })).rejects.toMatchObject({
|
|
1231
|
+
code: 'SHEET_BUSY',
|
|
1232
|
+
});
|
|
1233
|
+
const { FiasBridgeError } = jest.requireActual('./bridge');
|
|
1234
|
+
mockBridge.request.mockRejectedValueOnce(new FiasBridgeError('no client-side executor registered for x', 'BRIDGE_ERROR'));
|
|
1235
|
+
await expect(useOpener().send({ fileId: FILE })).rejects.toMatchObject({
|
|
1236
|
+
code: 'SEND_UNAVAILABLE',
|
|
1237
|
+
});
|
|
1238
|
+
});
|
|
1239
|
+
});
|
|
1056
1240
|
describe('useFiasConnections', () => {
|
|
1057
1241
|
it('lists through the connections entity', async () => {
|
|
1058
1242
|
const { useFiasConnections } = require('./hooks');
|