@dotdrelle/wiki-manager 0.15.85 → 0.15.91

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.
@@ -9,6 +9,7 @@ import {
9
9
  chatAllowedTools,
10
10
  createSession,
11
11
  isProductHelpQuestion,
12
+ readSelectedPageDocuments,
12
13
  runHeadlessChatTurn,
13
14
  sanitizeOpenWikiPage,
14
15
  sanitizeOpenWikiPages,
@@ -763,6 +764,11 @@ test('built-in /status keeps priority while /skills run status explicitly reache
763
764
  requests.push({ path: pathOf(url), body: options.body ? JSON.parse(options.body) : null });
764
765
  return jsonResponse(202, { accepted: true, kind: 'skill_chain', objectives: 1 });
765
766
  });
767
+ // The machine's manager .env must not leak a GATEWAY_ENABLED=true into this
768
+ // test: /status would then discover the gateway and fetch /health +
769
+ // /capabilities, and the assertion below reads those as status noise.
770
+ const previousEnvFile = process.env.WIKI_MANAGER_ENV_FILE;
771
+ process.env.WIKI_MANAGER_ENV_FILE = join(root, 'test.env');
766
772
  try {
767
773
  await runLine('/status', { agent: null, packageJson: { version: 'test' }, session, runtime: { url: 'http://runtime.test' } });
768
774
  assert.equal(requests.length, 0, 'built-in status must stay local');
@@ -770,7 +776,11 @@ test('built-in /status keeps priority while /skills run status explicitly reache
770
776
  assert.equal(requests[0].path, '/run');
771
777
  assert.equal(requests[0].body.skillName, 'status');
772
778
  assert.equal(requests[0].body.input, '/status');
773
- } finally { restore(); }
779
+ } finally {
780
+ restore();
781
+ if (previousEnvFile === undefined) delete process.env.WIKI_MANAGER_ENV_FILE;
782
+ else process.env.WIKI_MANAGER_ENV_FILE = previousEnvFile;
783
+ }
774
784
  });
775
785
 
776
786
  test('runLine does not update workspace profile before Donna handles the request', async () => {
@@ -1247,6 +1257,81 @@ test('runHeadlessChatTurn inlines selected document content (multiple files) so
1247
1257
  }
1248
1258
  });
1249
1259
 
1260
+ test('runHeadlessChatTurn follows a selected digest page\'s [src: ...] citation and attaches the real source too', async () => {
1261
+ const root = mkdtempSync(join(tmpdir(), 'repl-docs-cite-'));
1262
+ mkdirSync(join(root, 'raw', 'ingested', 'topic'), { recursive: true });
1263
+ mkdirSync(join(root, 'wiki', 'concepts', 'produit'), { recursive: true });
1264
+ const sourcePath = 'raw/ingested/topic/full-source.md';
1265
+ writeFileSync(join(root, sourcePath), 'CONTENU_SOURCE_COMPLET avec tous les détails fonctionnels');
1266
+ writeFileSync(
1267
+ join(root, 'wiki', 'concepts', 'produit', 'digest.md'),
1268
+ `# Digest\n\nCONTENU_DIGEST_COURT. [src: ${sourcePath}]`,
1269
+ );
1270
+ try {
1271
+ const session = createSession();
1272
+ session.chatMode = true;
1273
+ session.workspacePath = root;
1274
+ session.chatAccess = { maxToolIterations: 4, servers: {} };
1275
+ session.mcp = {};
1276
+ // No tools declared (empty chatAccess.servers), so this exercises the
1277
+ // plain-stream branch — the one case where nothing else could have
1278
+ // supplied the source content except the citation-following itself.
1279
+ let seenMessages = [];
1280
+ session.llm = {
1281
+ async *stream({ messages }) {
1282
+ seenMessages = messages ?? [];
1283
+ yield 'ok';
1284
+ },
1285
+ };
1286
+ await runHeadlessChatTurn(session, 'résume ce document', {
1287
+ history: [],
1288
+ openWikiPages: ['wiki/concepts/produit/digest.md'],
1289
+ });
1290
+ const joined = seenMessages.map((message) => String(message.content ?? '')).join('\n');
1291
+ assert.match(joined, /CONTENU_DIGEST_COURT/);
1292
+ assert.match(joined, /CONTENU_SOURCE_COMPLET avec tous les détails fonctionnels/);
1293
+ assert.match(joined, new RegExp(`cited source of wiki/concepts/produit/digest\\.md`));
1294
+ } finally {
1295
+ rmSync(root, { recursive: true, force: true });
1296
+ }
1297
+ });
1298
+
1299
+ test('readSelectedPageDocuments reads a repeated citation once and does not follow it a second level', async () => {
1300
+ const root = mkdtempSync(join(tmpdir(), 'repl-docs-cite-dedup-'));
1301
+ mkdirSync(join(root, 'raw', 'ingested'), { recursive: true });
1302
+ mkdirSync(join(root, 'wiki', 'concepts', 'produit'), { recursive: true });
1303
+ const sourcePath = 'raw/ingested/shared-source.md';
1304
+ // The shared source itself carries a [src: ...] marker (to a page that is
1305
+ // never attached elsewhere) — proving the follow stops at one level: it
1306
+ // must not appear as its own separate attached document.
1307
+ const secondLevelPath = 'raw/ingested/never-attached.md';
1308
+ writeFileSync(join(root, sourcePath), `CONTENU_SOURCE_PARTAGEE [src: ${secondLevelPath}]`);
1309
+ writeFileSync(join(root, secondLevelPath), 'CONTENU_JAMAIS_ATTACHE');
1310
+ // Two digests both cite the SAME source, and one cites it twice.
1311
+ writeFileSync(
1312
+ join(root, 'wiki', 'concepts', 'produit', 'digest-a.md'),
1313
+ `DIGEST_A premier extrait [src: ${sourcePath}] et un second rappel [src: ${sourcePath}]`,
1314
+ );
1315
+ writeFileSync(
1316
+ join(root, 'wiki', 'concepts', 'produit', 'digest-b.md'),
1317
+ `DIGEST_B autre angle [src: ${sourcePath}]`,
1318
+ );
1319
+ try {
1320
+ const session = createSession();
1321
+ session.workspacePath = root;
1322
+ const docs = await readSelectedPageDocuments(session, [
1323
+ 'wiki/concepts/produit/digest-a.md',
1324
+ 'wiki/concepts/produit/digest-b.md',
1325
+ ]);
1326
+ const sourceDocs = docs.filter((doc) => doc.path === sourcePath);
1327
+ assert.equal(sourceDocs.length, 1, 'the shared source must be attached exactly once, not once per citing digest');
1328
+ assert.match(sourceDocs[0].content, /CONTENU_SOURCE_PARTAGEE/);
1329
+ assert.equal(docs.some((doc) => doc.path === secondLevelPath), false, 'a citation inside the followed source must not itself be followed');
1330
+ } finally {
1331
+ rmSync(root, { recursive: true, force: true });
1332
+ }
1333
+ });
1334
+
1250
1335
  test('runHeadlessChatTurn falls back to the plain stream without read tools', async () => {
1251
1336
  const session = createSession();
1252
1337
  session.chatMode = true;
package/wiki-workspace CHANGED
@@ -1188,7 +1188,13 @@ refresh_running_services() {
1188
1188
  local running_services=()
1189
1189
  read_lines_into_array running_services "$@" ps --status running --services
1190
1190
  if [[ ${#running_services[@]} -eq 0 ]]; then
1191
- printf '%s\n' "$no_running_msg"
1191
+ # Refreshing only what already runs made `refresh` a no-op on a cold stack —
1192
+ # exactly the state after a reboot, which is when someone runs it. The images
1193
+ # are the thing being refreshed, and pulling them does not need a container
1194
+ # up; only the recreate does.
1195
+ printf '%s — pulling images anyway so the next start uses them.\n' "$no_running_msg"
1196
+ "$@" pull
1197
+ printf 'Images pulled. Start the stack to run them.\n'
1192
1198
  return
1193
1199
  fi
1194
1200
  "$@" pull "${running_services[@]}"