cozy-search 2.4.0 → 2.5.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/README.md CHANGED
@@ -61,12 +61,19 @@ of verbs (cozy-home and cozy-drive use `ALL`).
61
61
 
62
62
  Opening the assistant (`AssistantView` or `AssistantDialog`) calls
63
63
  `autoprovisionAssistants(client)` once per session: when the
64
- `cozy.assistant.autoprovision` flag lists assistants, it runs
65
- `setupRagIndexing(client)` then `ensureProvisionedAssistants(client, entries)`.
66
- A host app that wants this done at startup, before the assistant is
67
- opened, can call `autoprovisionAssistants(client)` itself (or the
68
- `useAssistantsAutoprovision` hook): every call shares the first one's
69
- promise, so the two never race.
64
+ `cozy.assistant.autoprovision` flag lists assistants, it makes sure the
65
+ `rag-index` triggers exist, gives the root folder to the assistants that
66
+ have no knowledge base folder, then runs
67
+ `ensureProvisionedAssistants(client, entries)`.
68
+
69
+ A host app should only call the `useRagIndexTriggers` hook (or
70
+ `ensureRagIndexTriggers(client)`) at startup: one request when the
71
+ triggers already exist, and the knowledge base folders get indexed as
72
+ they change before the assistant is ever opened. The assistants are
73
+ provisioned when the assistant opens; a host app that wants that earlier
74
+ can call `autoprovisionAssistants(client)` or the
75
+ `useAssistantsAutoprovision` hook. Every call shares the first one's
76
+ promise, the triggers step included, so nothing runs twice.
70
77
 
71
78
  `setupRagIndexing(client)` makes sure the instance's two
72
79
  `rag-index` triggers exist (one on `io.cozy.files`, one on
@@ -1,4 +1,9 @@
1
1
  export function resetAutoprovisionForTests(): void;
2
+ export function autoprovisionEntries(): Array<object> | null;
3
+ export function ensureRagIndexTriggers(client: import("cozy-client").CozyClient): Promise<{
4
+ triggers: string[];
5
+ error: Error | null;
6
+ }>;
2
7
  export function autoprovisionAssistants(client: import("cozy-client").CozyClient): Promise<null | {
3
8
  setup: object;
4
9
  created: string[];
@@ -5,7 +5,7 @@ var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefau
5
5
  Object.defineProperty(exports, "__esModule", {
6
6
  value: true
7
7
  });
8
- exports.resetAutoprovisionForTests = exports.autoprovisionAssistants = void 0;
8
+ exports.resetAutoprovisionForTests = exports.ensureRagIndexTriggers = exports.autoprovisionEntries = exports.autoprovisionAssistants = void 0;
9
9
 
10
10
  var _regenerator = _interopRequireDefault(require("@babel/runtime/regenerator"));
11
11
 
@@ -34,24 +34,70 @@ var warn = function warn() {
34
34
  (_console = console).warn.apply(_console, ['cozy-search autoprovision:'].concat(args));
35
35
  };
36
36
 
37
+ var triggersPending = null;
37
38
  var pending = null;
38
39
 
39
40
  var resetAutoprovisionForTests = function resetAutoprovisionForTests() {
41
+ triggersPending = null;
40
42
  pending = null;
41
43
  };
44
+ /**
45
+ * The entries of the autoprovision flag, or null when it lists nothing.
46
+ * @returns {Array<object>|null}
47
+ */
48
+
42
49
 
43
50
  exports.resetAutoprovisionForTests = resetAutoprovisionForTests;
44
51
 
52
+ var autoprovisionEntries = function autoprovisionEntries() {
53
+ var entries = (0, _cozyFlags.default)(_provisioning.AUTOPROVISION_FLAG);
54
+ return Array.isArray(entries) && entries.length > 0 ? entries : null;
55
+ };
56
+ /**
57
+ * Makes sure the instance has its rag-index triggers, so the files of the
58
+ * knowledge base folders get indexed as they change. Runs once per session
59
+ * and never rejects: a host app can call it at startup, it costs a single
60
+ * request when the triggers already exist.
61
+ * @param {import('cozy-client').CozyClient} client - The cozy client.
62
+ * @returns {Promise<{triggers: string[], error: Error|null}>} The
63
+ * `arguments` of the triggers created, and the failure if any.
64
+ */
65
+
66
+
67
+ exports.autoprovisionEntries = autoprovisionEntries;
68
+
69
+ var ensureRagIndexTriggers = function ensureRagIndexTriggers(client) {
70
+ if (!triggersPending) {
71
+ triggersPending = (0, _ragIndexing.createRagIndexTriggers)(client).then(function (triggers) {
72
+ return {
73
+ triggers: triggers,
74
+ error: null
75
+ };
76
+ }, function (error) {
77
+ warn('cannot set up the rag-index triggers', error);
78
+ return {
79
+ triggers: [],
80
+ error: error
81
+ };
82
+ });
83
+ }
84
+
85
+ return triggersPending;
86
+ };
87
+
88
+ exports.ensureRagIndexTriggers = ensureRagIndexTriggers;
89
+
45
90
  var run = /*#__PURE__*/function () {
46
91
  var _ref = (0, _asyncToGenerator2.default)( /*#__PURE__*/_regenerator.default.mark(function _callee(client) {
47
- var entries, setup, result;
92
+ var entries, setup, _yield$ensureRagIndex, triggers, error, assistants, result;
93
+
48
94
  return _regenerator.default.wrap(function _callee$(_context) {
49
95
  while (1) {
50
96
  switch (_context.prev = _context.next) {
51
97
  case 0:
52
- entries = (0, _cozyFlags.default)(_provisioning.AUTOPROVISION_FLAG);
98
+ entries = autoprovisionEntries();
53
99
 
54
- if (!(!Array.isArray(entries) || entries.length === 0)) {
100
+ if (entries) {
55
101
  _context.next = 3;
56
102
  break;
57
103
  }
@@ -59,15 +105,48 @@ var run = /*#__PURE__*/function () {
59
105
  return _context.abrupt("return", null);
60
106
 
61
107
  case 3:
62
- _context.next = 5;
63
- return (0, _ragIndexing.setupRagIndexing)(client);
64
-
65
- case 5:
66
- setup = _context.sent;
67
- _context.next = 8;
68
- return (0, _provisioning.ensureProvisionedAssistants)(client, entries);
69
-
70
- case 8:
108
+ setup = {
109
+ triggers: [],
110
+ migrated: [],
111
+ errors: []
112
+ };
113
+ _context.next = 6;
114
+ return ensureRagIndexTriggers(client);
115
+
116
+ case 6:
117
+ _yield$ensureRagIndex = _context.sent;
118
+ triggers = _yield$ensureRagIndex.triggers;
119
+ error = _yield$ensureRagIndex.error;
120
+ setup.triggers = triggers;
121
+ if (error) setup.errors.push(error);
122
+ assistants = null;
123
+ _context.prev = 12;
124
+ _context.next = 15;
125
+ return (0, _ragIndexing.fetchAssistants)(client);
126
+
127
+ case 15:
128
+ assistants = _context.sent;
129
+ _context.next = 18;
130
+ return (0, _ragIndexing.migrateAssistantsWithoutFolder)(client, assistants);
131
+
132
+ case 18:
133
+ setup.migrated = _context.sent;
134
+ _context.next = 25;
135
+ break;
136
+
137
+ case 21:
138
+ _context.prev = 21;
139
+ _context.t0 = _context["catch"](12);
140
+ warn('cannot migrate the assistants without folder', _context.t0);
141
+ setup.errors.push(_context.t0);
142
+
143
+ case 25:
144
+ _context.next = 27;
145
+ return (0, _provisioning.ensureProvisionedAssistants)(client, entries, {
146
+ assistants: assistants
147
+ });
148
+
149
+ case 27:
71
150
  result = _context.sent;
72
151
 
73
152
  if (result.skipped.length > 0) {
@@ -78,12 +157,12 @@ var run = /*#__PURE__*/function () {
78
157
  setup: setup
79
158
  }, result));
80
159
 
81
- case 11:
160
+ case 30:
82
161
  case "end":
83
162
  return _context.stop();
84
163
  }
85
164
  }
86
- }, _callee);
165
+ }, _callee, null, [[12, 21]]);
87
166
  }));
88
167
 
89
168
  return function run(_x) {
@@ -91,10 +170,12 @@ var run = /*#__PURE__*/function () {
91
170
  };
92
171
  }();
93
172
  /**
94
- * Sets up the rag-index triggers, then provisions the assistants listed in
95
- * the autoprovision flag. Runs once per session: every call shares the
96
- * first one's promise, so the assistant UI and the host app's startup can
97
- * both trigger it without racing each other. Never rejects.
173
+ * Sets up the rag-index triggers, gives a folder to the assistants that
174
+ * lack one, then provisions the assistants listed in the autoprovision
175
+ * flag. Runs once per session: every call shares the first one's promise,
176
+ * and the triggers step is shared with ensureRagIndexTriggers, so a host
177
+ * app that ensured them at startup does not pay for them twice. Never
178
+ * rejects.
98
179
  * @param {import('cozy-client').CozyClient} client - The cozy client.
99
180
  * @returns {Promise<null|{setup: object, created: string[], ensured: string[], skipped: {id: string, reason: string}[]}>} Null when the flag lists nothing.
100
181
  */
@@ -2,59 +2,135 @@ import flag from 'cozy-flags'
2
2
 
3
3
  import {
4
4
  autoprovisionAssistants,
5
+ autoprovisionEntries,
6
+ ensureRagIndexTriggers,
5
7
  resetAutoprovisionForTests
6
8
  } from './autoprovision'
7
9
  import { ensureProvisionedAssistants } from './provisioning'
8
- import { setupRagIndexing } from './ragIndexing'
10
+ import {
11
+ createRagIndexTriggers,
12
+ fetchAssistants,
13
+ migrateAssistantsWithoutFolder
14
+ } from './ragIndexing'
9
15
 
10
16
  jest.mock('cozy-flags', () => jest.fn())
11
17
  jest.mock('./provisioning', () => ({
12
18
  AUTOPROVISION_FLAG: 'cozy.assistant.autoprovision',
13
19
  ensureProvisionedAssistants: jest.fn()
14
20
  }))
15
- jest.mock('./ragIndexing', () => ({ setupRagIndexing: jest.fn() }))
21
+ jest.mock('./ragIndexing', () => ({
22
+ createRagIndexTriggers: jest.fn(),
23
+ fetchAssistants: jest.fn(),
24
+ migrateAssistantsWithoutFolder: jest.fn()
25
+ }))
16
26
 
17
27
  const client = { id: 'client' }
18
28
  const entries = [{ name: 'Docs', dirName: 'Docs' }]
29
+ const assistants = [{ _id: 'legacy' }]
19
30
  let warnSpy
20
31
 
21
- describe('autoprovisionAssistants', () => {
22
- beforeEach(() => {
23
- resetAutoprovisionForTests()
24
- flag.mockReset()
25
-
26
- warnSpy = jest.spyOn(console, 'warn').mockImplementation(() => {})
27
- setupRagIndexing
28
- .mockReset()
29
- .mockResolvedValue({ triggers: [], migrated: [], errors: [] })
30
- ensureProvisionedAssistants
31
- .mockReset()
32
- .mockResolvedValue({ created: ['docs'], ensured: [], skipped: [] })
32
+ beforeEach(() => {
33
+ resetAutoprovisionForTests()
34
+ flag.mockReset()
35
+ warnSpy = jest.spyOn(console, 'warn').mockImplementation(() => {})
36
+ createRagIndexTriggers.mockReset().mockResolvedValue(['io.cozy.files'])
37
+ fetchAssistants.mockReset().mockResolvedValue(assistants)
38
+ migrateAssistantsWithoutFolder.mockReset().mockResolvedValue([])
39
+ ensureProvisionedAssistants
40
+ .mockReset()
41
+ .mockResolvedValue({ created: ['docs'], ensured: [], skipped: [] })
42
+ })
43
+
44
+ afterEach(() => {
45
+ warnSpy.mockRestore()
46
+ })
47
+
48
+ describe('autoprovisionEntries', () => {
49
+ it('returns the flag entries', () => {
50
+ flag.mockReturnValue(entries)
51
+ expect(autoprovisionEntries()).toBe(entries)
52
+ expect(flag).toHaveBeenCalledWith('cozy.assistant.autoprovision')
53
+ })
54
+
55
+ it.each([null, undefined, [], 'docs', {}])('returns null for %p', value => {
56
+ flag.mockReturnValue(value)
57
+ expect(autoprovisionEntries()).toBeNull()
33
58
  })
59
+ })
34
60
 
35
- afterEach(() => {
36
- warnSpy.mockRestore()
61
+ describe('ensureRagIndexTriggers', () => {
62
+ it('creates the missing triggers once per session', async () => {
63
+ const first = ensureRagIndexTriggers(client)
64
+ expect(ensureRagIndexTriggers(client)).toBe(first)
65
+ await expect(first).resolves.toEqual({
66
+ triggers: ['io.cozy.files'],
67
+ error: null
68
+ })
69
+ await ensureRagIndexTriggers(client)
70
+
71
+ expect(createRagIndexTriggers).toHaveBeenCalledTimes(1)
72
+ expect(createRagIndexTriggers).toHaveBeenCalledWith(client)
37
73
  })
38
74
 
75
+ it('reports a failure without rejecting', async () => {
76
+ const error = new Error('forbidden')
77
+ createRagIndexTriggers.mockRejectedValue(error)
78
+ await expect(ensureRagIndexTriggers(client)).resolves.toEqual({
79
+ triggers: [],
80
+ error
81
+ })
82
+ expect(warnSpy).toHaveBeenCalledWith(
83
+ 'cozy-search autoprovision:',
84
+ 'cannot set up the rag-index triggers',
85
+ error
86
+ )
87
+ })
88
+ })
89
+
90
+ describe('autoprovisionAssistants', () => {
39
91
  it('does nothing without flag entries', async () => {
40
92
  flag.mockReturnValue(null)
41
93
  await expect(autoprovisionAssistants(client)).resolves.toBeNull()
42
- expect(flag).toHaveBeenCalledWith('cozy.assistant.autoprovision')
43
- expect(setupRagIndexing).not.toHaveBeenCalled()
94
+ expect(createRagIndexTriggers).not.toHaveBeenCalled()
95
+ expect(fetchAssistants).not.toHaveBeenCalled()
96
+ expect(migrateAssistantsWithoutFolder).not.toHaveBeenCalled()
44
97
  expect(ensureProvisionedAssistants).not.toHaveBeenCalled()
45
98
  })
46
99
 
47
- it('sets up the indexing, then provisions the flag entries', async () => {
100
+ it('lets the provisioning query the assistants when listing them failed', async () => {
101
+ flag.mockReturnValue(entries)
102
+ fetchAssistants.mockRejectedValue(new Error('nope'))
103
+ const result = await autoprovisionAssistants(client)
104
+
105
+ expect(migrateAssistantsWithoutFolder).not.toHaveBeenCalled()
106
+ expect(ensureProvisionedAssistants).toHaveBeenCalledWith(client, entries, {
107
+ assistants: null
108
+ })
109
+ expect(result.setup.errors.map(e => e.message)).toEqual(['nope'])
110
+ })
111
+
112
+ it('ensures the triggers, migrates, then provisions the flag entries', async () => {
48
113
  flag.mockReturnValue(entries)
114
+ migrateAssistantsWithoutFolder.mockResolvedValue(['legacy'])
49
115
  const result = await autoprovisionAssistants(client)
50
116
 
51
- expect(setupRagIndexing).toHaveBeenCalledWith(client)
52
- expect(ensureProvisionedAssistants).toHaveBeenCalledWith(client, entries)
53
- expect(setupRagIndexing.mock.invocationCallOrder[0]).toBeLessThan(
54
- ensureProvisionedAssistants.mock.invocationCallOrder[0]
117
+ expect(createRagIndexTriggers).toHaveBeenCalledWith(client)
118
+ expect(fetchAssistants).toHaveBeenCalledTimes(1)
119
+ expect(migrateAssistantsWithoutFolder).toHaveBeenCalledWith(
120
+ client,
121
+ assistants
55
122
  )
123
+ expect(ensureProvisionedAssistants).toHaveBeenCalledWith(client, entries, {
124
+ assistants
125
+ })
126
+ const order = [
127
+ createRagIndexTriggers,
128
+ migrateAssistantsWithoutFolder,
129
+ ensureProvisionedAssistants
130
+ ].map(fn => fn.mock.invocationCallOrder[0])
131
+ expect(order).toEqual([...order].sort((a, b) => a - b))
56
132
  expect(result).toEqual({
57
- setup: { triggers: [], migrated: [], errors: [] },
133
+ setup: { triggers: ['io.cozy.files'], migrated: ['legacy'], errors: [] },
58
134
  created: ['docs'],
59
135
  ensured: [],
60
136
  skipped: []
@@ -64,26 +140,34 @@ describe('autoprovisionAssistants', () => {
64
140
  it('runs once per session, sharing the promise', async () => {
65
141
  flag.mockReturnValue(entries)
66
142
  const first = autoprovisionAssistants(client)
67
- const second = autoprovisionAssistants(client)
68
- expect(second).toBe(first)
143
+ expect(autoprovisionAssistants(client)).toBe(first)
69
144
  await first
70
145
  await autoprovisionAssistants(client)
71
146
 
72
- expect(setupRagIndexing).toHaveBeenCalledTimes(1)
147
+ expect(createRagIndexTriggers).toHaveBeenCalledTimes(1)
73
148
  expect(ensureProvisionedAssistants).toHaveBeenCalledTimes(1)
74
149
  })
75
150
 
76
- it('still provisions when the setup reported errors', async () => {
151
+ it('shares the triggers step with ensureRagIndexTriggers', async () => {
77
152
  flag.mockReturnValue(entries)
78
- setupRagIndexing.mockResolvedValue({
79
- triggers: [],
80
- migrated: [],
81
- errors: [new Error('forbidden')]
82
- })
153
+ await ensureRagIndexTriggers(client)
154
+ await autoprovisionAssistants(client)
155
+
156
+ expect(createRagIndexTriggers).toHaveBeenCalledTimes(1)
157
+ expect(ensureProvisionedAssistants).toHaveBeenCalledTimes(1)
158
+ })
159
+
160
+ it('still provisions when the setup steps failed', async () => {
161
+ flag.mockReturnValue(entries)
162
+ createRagIndexTriggers.mockRejectedValue(new Error('forbidden'))
163
+ migrateAssistantsWithoutFolder.mockRejectedValue(new Error('nope'))
83
164
  const result = await autoprovisionAssistants(client)
84
165
 
85
166
  expect(ensureProvisionedAssistants).toHaveBeenCalledTimes(1)
86
- expect(result.setup.errors).toHaveLength(1)
167
+ expect(result.setup.errors.map(e => e.message)).toEqual([
168
+ 'forbidden',
169
+ 'nope'
170
+ ])
87
171
  })
88
172
 
89
173
  it('logs the skipped entries', async () => {
@@ -104,7 +188,7 @@ describe('autoprovisionAssistants', () => {
104
188
 
105
189
  it('never rejects', async () => {
106
190
  flag.mockReturnValue(entries)
107
- setupRagIndexing.mockRejectedValue(new Error('boom'))
191
+ ensureProvisionedAssistants.mockRejectedValue(new Error('boom'))
108
192
  await expect(autoprovisionAssistants(client)).resolves.toBeNull()
109
193
  expect(warnSpy).toHaveBeenCalledWith(
110
194
  'cozy-search autoprovision:',
@@ -14,7 +14,9 @@ export function ensureProvisionedAssistants(client: import("cozy-client").CozyCl
14
14
  prompt?: string;
15
15
  icon?: string | null;
16
16
  default?: boolean;
17
- }>): Promise<{
17
+ }>, { assistants }?: {
18
+ assistants?: object[] | undefined;
19
+ } | undefined): Promise<{
18
20
  created: string[];
19
21
  ensured: string[];
20
22
  skipped: {