@sanqianx/project-knowledge 5.0.26 → 5.0.28
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/.claude-plugin/marketplace.json +1 -1
- package/_modules/knowledge-engine/README.md +2 -0
- package/_modules/knowledge-engine/package.json +1 -1
- package/_modules/knowledge-engine/src/memories.js +50 -5
- package/_modules/knowledge-engine/src/server.js +5 -0
- package/_modules/knowledge-engine/src/store.js +12 -2
- package/_site/_test/helpers/qa-r44-env.js +418 -0
- package/_site/_test/helpers/qa-r44-report.js +656 -0
- package/_site/_test/helpers/qa-r44-ui.js +347 -0
- package/_site/_test/helpers/qa-r49-wb-host.js +58 -0
- package/_site/_test/helpers/qa-r91-harness.cjs +531 -0
- package/_site/_test/qa-r44-permission-question.js +756 -0
- package/_site/_test/qa-r49-sys003-real-blank-probe.js +374 -0
- package/module-runtime-manifest.json +35 -32
- package/package.json +2 -2
- package/plugins/project-knowledge/.claude-plugin/plugin.json +1 -1
- package/plugins/project-knowledge/.codex-plugin/plugin.json +1 -1
- package/plugins/project-knowledge/.mcp.json +1 -1
- package/ui/index.html +1 -0
- package/ui/product.css +1 -1
- package/ui/product.js +28 -2
- package/vendor-manifest.json +7 -7
|
@@ -0,0 +1,347 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
// _site/_test/helpers/qa-r44-ui.js
|
|
3
|
+
//
|
|
4
|
+
// The R44 lane's real-UI actions on the product's own permission and
|
|
5
|
+
// question cards.
|
|
6
|
+
//
|
|
7
|
+
// Every function below drives the DOM control the product itself renders:
|
|
8
|
+
// the 允许 / 拒绝 buttons of the tool permission card, the radio / checkbox /
|
|
9
|
+
// custom input and the 提交答案 button of the question card, and the
|
|
10
|
+
// product's own permission-mode menu. Nothing here fabricates an event, calls
|
|
11
|
+
// a product endpoint in place of the UI, or writes a card state by hand.
|
|
12
|
+
//
|
|
13
|
+
// The product marks its own cards:
|
|
14
|
+
// tool card <div class="perm" data-perm-id="..."> with .p-title,
|
|
15
|
+
// .p-sub, .btn-allow (允许), .btn-deny (拒绝) and, once it
|
|
16
|
+
// settles, .p-state (已允许 / 已拒绝)
|
|
17
|
+
// question card <section class="perm question-card" data-perm-id="...">
|
|
18
|
+
// with one .question-field per question, .question-option
|
|
19
|
+
// labels, an optional .question-custom input and a
|
|
20
|
+
// .question-submit button (提交答案)
|
|
21
|
+
|
|
22
|
+
function wait(ms) { return new Promise(resolve => setTimeout(resolve, ms)); }
|
|
23
|
+
|
|
24
|
+
async function waitUntil(fn, label, timeoutMs = 30000, intervalMs = 150) {
|
|
25
|
+
const deadline = Date.now() + timeoutMs;
|
|
26
|
+
let last;
|
|
27
|
+
while (Date.now() < deadline) {
|
|
28
|
+
try {
|
|
29
|
+
const value = await fn();
|
|
30
|
+
if (value) return value;
|
|
31
|
+
last = value;
|
|
32
|
+
} catch (error) { last = error && error.message; }
|
|
33
|
+
await wait(intervalMs);
|
|
34
|
+
}
|
|
35
|
+
const detail = last === undefined ? '' : ` (last: ${typeof last === 'string' ? last : JSON.stringify(last)})`;
|
|
36
|
+
throw new Error(`timed out after ${timeoutMs}ms waiting for ${label}${detail}`);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// --- the tool permission card ----------------------------------------------
|
|
40
|
+
|
|
41
|
+
const READ_TOOL_CARDS = `(() => {
|
|
42
|
+
const cards = Array.from(document.querySelectorAll('.perm[data-perm-id]')).filter(node => !node.classList.contains('question-card'));
|
|
43
|
+
return cards.map(node => {
|
|
44
|
+
const title = node.querySelector('.p-title');
|
|
45
|
+
const sub = node.querySelector('.p-sub');
|
|
46
|
+
const state = node.querySelector('.p-state');
|
|
47
|
+
const allow = node.querySelector('.btn-allow');
|
|
48
|
+
const deny = node.querySelector('.btn-deny');
|
|
49
|
+
const titleText = ((title && title.textContent) || '').trim();
|
|
50
|
+
return {
|
|
51
|
+
requestId: node.dataset.permId || '',
|
|
52
|
+
title: titleText,
|
|
53
|
+
toolName: titleText.split(/\\s+/)[0] || '',
|
|
54
|
+
sub: ((sub && sub.textContent) || '').trim(),
|
|
55
|
+
hasAllow: Boolean(allow),
|
|
56
|
+
hasDeny: Boolean(deny),
|
|
57
|
+
settledState: state ? (state.textContent || '').trim() : null,
|
|
58
|
+
settled: Boolean(state),
|
|
59
|
+
cardClass: node.className,
|
|
60
|
+
};
|
|
61
|
+
});
|
|
62
|
+
})()`;
|
|
63
|
+
|
|
64
|
+
async function readToolPermissionCards(browser) {
|
|
65
|
+
return browser.evaluate(READ_TOOL_CARDS);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// The card of one specific request. The product renders the tool name of a
|
|
69
|
+
// permission card only in the EVENT summary, not in the card title (the card
|
|
70
|
+
// reads "Agent 请求执行操作"), so the card is identified by the request id
|
|
71
|
+
// the product itself put into `data-perm-id`.
|
|
72
|
+
async function findToolCard(browser, requestId) {
|
|
73
|
+
const cards = await readToolPermissionCards(browser);
|
|
74
|
+
return cards.find(card => card.requestId === requestId) || null;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// The event streams the PAGE itself opened, read from the recorder the
|
|
78
|
+
// frozen harness injects into every page it opens. The lane uses it to know
|
|
79
|
+
// that the page is subscribed to a session BEFORE a turn is sent.
|
|
80
|
+
async function readPageStreams(browser) {
|
|
81
|
+
return browser.evaluate(`(() => {
|
|
82
|
+
const streams = (window.__wbRecorder && window.__wbRecorder.streams) || [];
|
|
83
|
+
return streams.map(entry => String(entry.url || ''));
|
|
84
|
+
})()`);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// The events the PAGE itself received, read from the recorder the frozen
|
|
88
|
+
// harness injects into every page it opens. This is the browser's own
|
|
89
|
+
// receipt of the product's event stream.
|
|
90
|
+
async function readPageEvents(browser, type) {
|
|
91
|
+
return browser.evaluate(`(() => {
|
|
92
|
+
const log = (window.__wbRecorder && window.__wbRecorder.log) || [];
|
|
93
|
+
const wanted = ${JSON.stringify(type || null)};
|
|
94
|
+
const out = [];
|
|
95
|
+
for (const entry of log) {
|
|
96
|
+
if (wanted && entry.type !== wanted) continue;
|
|
97
|
+
let event = null;
|
|
98
|
+
try { event = JSON.parse(entry.data); } catch (error) { event = null; }
|
|
99
|
+
out.push({ type: entry.type, event });
|
|
100
|
+
}
|
|
101
|
+
return out;
|
|
102
|
+
})()`);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// What the page shows right now: the permission cards, the question cards and
|
|
106
|
+
// the product's own session state. Used as the driver-side diagnostic.
|
|
107
|
+
async function readDomDiagnostics(browser) {
|
|
108
|
+
return browser.evaluate(`(() => {
|
|
109
|
+
const col = document.getElementById('stream-col');
|
|
110
|
+
return {
|
|
111
|
+
permCount: document.querySelectorAll('.perm').length,
|
|
112
|
+
permIds: Array.from(document.querySelectorAll('.perm[data-perm-id]')).map(node => node.dataset.permId || ''),
|
|
113
|
+
questionCardCount: document.querySelectorAll('.perm.question-card').length,
|
|
114
|
+
streamChildren: col ? col.childElementCount : null,
|
|
115
|
+
streamTextLength: col ? (col.textContent || '').length : null,
|
|
116
|
+
sessionState: (typeof state !== 'undefined' && state) ? (state.sessionState || null) : null,
|
|
117
|
+
activeSessionId: (typeof state !== 'undefined' && state) ? (state.activeSessionId || null) : null,
|
|
118
|
+
permissionMode: (typeof state !== 'undefined' && state) ? (state.permissionMode || null) : null,
|
|
119
|
+
recordedEventTypes: ((window.__wbRecorder && window.__wbRecorder.log) || []).map(entry => entry.type),
|
|
120
|
+
};
|
|
121
|
+
})()`);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// The product's own 拒绝 button of the card with that requestId.
|
|
125
|
+
async function clickDeny(browser, requestId) {
|
|
126
|
+
const result = await browser.evaluate(`(() => {
|
|
127
|
+
const id = ${JSON.stringify(requestId)};
|
|
128
|
+
const node = document.querySelector('.perm[data-perm-id="' + id.replace(/"/g, '\\\\"') + '"]:not(.question-card)');
|
|
129
|
+
if (!node) return { clicked: false, reason: 'no such permission card' };
|
|
130
|
+
const deny = node.querySelector('.btn-deny');
|
|
131
|
+
if (!deny) return { clicked: false, reason: 'the card has no 拒绝 button' };
|
|
132
|
+
deny.click();
|
|
133
|
+
return { clicked: true, title: ((node.querySelector('.p-title') || {}).textContent || '').trim() };
|
|
134
|
+
})()`);
|
|
135
|
+
if (!result.clicked) throw new Error(`the 拒绝 button could not be clicked: ${result.reason || 'unknown'}`);
|
|
136
|
+
return result;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// The product's own 允许 button of the card with that requestId.
|
|
140
|
+
async function clickAllow(browser, requestId) {
|
|
141
|
+
const result = await browser.evaluate(`(() => {
|
|
142
|
+
const id = ${JSON.stringify(requestId)};
|
|
143
|
+
const node = document.querySelector('.perm[data-perm-id="' + id.replace(/"/g, '\\\\"') + '"]:not(.question-card)');
|
|
144
|
+
if (!node) return { clicked: false, reason: 'no such permission card' };
|
|
145
|
+
const allow = node.querySelector('.btn-allow');
|
|
146
|
+
if (!allow) return { clicked: false, reason: 'the card has no 允许 button' };
|
|
147
|
+
allow.click();
|
|
148
|
+
return { clicked: true };
|
|
149
|
+
})()`);
|
|
150
|
+
if (!result.clicked) throw new Error(`the 允许 button could not be clicked: ${result.reason || 'unknown'}`);
|
|
151
|
+
return result;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// The state the product itself rendered after it settled the card.
|
|
155
|
+
async function readSettledCard(browser, requestId) {
|
|
156
|
+
return browser.evaluate(`(() => {
|
|
157
|
+
const id = ${JSON.stringify(requestId)};
|
|
158
|
+
const node = document.querySelector('.perm[data-perm-id="' + id.replace(/"/g, '\\\\"') + '"]');
|
|
159
|
+
if (!node) return { found: false };
|
|
160
|
+
const state = node.querySelector('.p-state');
|
|
161
|
+
return {
|
|
162
|
+
found: true,
|
|
163
|
+
isQuestionCard: node.classList.contains('question-card'),
|
|
164
|
+
settledState: state ? (state.textContent || '').trim() : null,
|
|
165
|
+
settled: node.classList.contains('question-settled') || node.classList.contains('settled'),
|
|
166
|
+
title: ((node.querySelector('.p-title') || node.querySelector('.question-title')) || {}).textContent || '',
|
|
167
|
+
cardClass: node.className,
|
|
168
|
+
};
|
|
169
|
+
})()`);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// --- the user question card ------------------------------------------------
|
|
173
|
+
|
|
174
|
+
const READ_QUESTION_CARDS = `(() => {
|
|
175
|
+
const cards = Array.from(document.querySelectorAll('.perm.question-card[data-perm-id]'));
|
|
176
|
+
return cards.map(node => {
|
|
177
|
+
const fields = Array.from(node.querySelectorAll('.question-field')).map(field => {
|
|
178
|
+
const legend = field.querySelector('legend');
|
|
179
|
+
const options = Array.from(field.querySelectorAll('.question-option')).map(label => {
|
|
180
|
+
const input = label.querySelector('input');
|
|
181
|
+
const text = label.querySelector('.question-option-label');
|
|
182
|
+
return {
|
|
183
|
+
label: ((text && text.textContent) || '').trim(),
|
|
184
|
+
inputType: input ? input.type : null,
|
|
185
|
+
checked: input ? input.checked === true : false,
|
|
186
|
+
value: input ? input.value : null,
|
|
187
|
+
};
|
|
188
|
+
});
|
|
189
|
+
const custom = field.querySelector('input.question-custom');
|
|
190
|
+
return {
|
|
191
|
+
legend: ((legend && legend.textContent) || '').trim(),
|
|
192
|
+
options,
|
|
193
|
+
customPresent: Boolean(custom),
|
|
194
|
+
customValue: custom ? custom.value : null,
|
|
195
|
+
customType: custom ? custom.type : null,
|
|
196
|
+
};
|
|
197
|
+
});
|
|
198
|
+
const submit = node.querySelector('.question-submit');
|
|
199
|
+
const title = node.querySelector('.question-title');
|
|
200
|
+
return {
|
|
201
|
+
requestId: node.dataset.permId || '',
|
|
202
|
+
title: ((title && title.textContent) || '').trim(),
|
|
203
|
+
settled: node.classList.contains('question-settled'),
|
|
204
|
+
fields,
|
|
205
|
+
submitDisabled: submit ? submit.disabled === true : null,
|
|
206
|
+
submitText: submit ? (submit.textContent || '').trim() : null,
|
|
207
|
+
errorText: ((node.querySelector('.question-error') || {}).textContent || '').trim(),
|
|
208
|
+
};
|
|
209
|
+
});
|
|
210
|
+
})()`;
|
|
211
|
+
|
|
212
|
+
async function readQuestionCards(browser) {
|
|
213
|
+
return browser.evaluate(READ_QUESTION_CARDS);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Click the radio of a single-select question by its visible label.
|
|
217
|
+
async function selectRadioOption(browser, requestId, fieldIndex, optionLabel) {
|
|
218
|
+
const result = await browser.evaluate(`(() => {
|
|
219
|
+
const id = ${JSON.stringify(requestId)};
|
|
220
|
+
const index = ${Number(fieldIndex)};
|
|
221
|
+
const label = ${JSON.stringify(optionLabel)};
|
|
222
|
+
const node = document.querySelector('.perm.question-card[data-perm-id="' + id.replace(/"/g, '\\\\"') + '"]');
|
|
223
|
+
if (!node) return { clicked: false, reason: 'no such question card' };
|
|
224
|
+
const field = node.querySelectorAll('.question-field')[index];
|
|
225
|
+
if (!field) return { clicked: false, reason: 'no such question field' };
|
|
226
|
+
for (const option of Array.from(field.querySelectorAll('.question-option'))) {
|
|
227
|
+
const text = option.querySelector('.question-option-label');
|
|
228
|
+
if (((text && text.textContent) || '').trim() === label) {
|
|
229
|
+
const input = option.querySelector('input');
|
|
230
|
+
if (!input) return { clicked: false, reason: 'the option has no input' };
|
|
231
|
+
input.click();
|
|
232
|
+
return { clicked: true, checked: input.checked === true, inputType: input.type };
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
return { clicked: false, reason: 'no option with that label' };
|
|
236
|
+
})()`);
|
|
237
|
+
if (!result.clicked) throw new Error(`the radio option could not be selected: ${result.reason || 'unknown'}`);
|
|
238
|
+
return result;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// Tick the checkboxes of a multi-select question by their visible labels.
|
|
242
|
+
async function selectCheckboxOptions(browser, requestId, fieldIndex, optionLabels) {
|
|
243
|
+
const labels = Array.isArray(optionLabels) ? optionLabels : [optionLabels];
|
|
244
|
+
const result = await browser.evaluate(`(() => {
|
|
245
|
+
const id = ${JSON.stringify(requestId)};
|
|
246
|
+
const index = ${Number(fieldIndex)};
|
|
247
|
+
const labels = ${JSON.stringify(labels)};
|
|
248
|
+
const node = document.querySelector('.perm.question-card[data-perm-id="' + id.replace(/"/g, '\\\\"') + '"]');
|
|
249
|
+
if (!node) return { clicked: false, reason: 'no such question card' };
|
|
250
|
+
const field = node.querySelectorAll('.question-field')[index];
|
|
251
|
+
if (!field) return { clicked: false, reason: 'no such question field' };
|
|
252
|
+
const picked = [];
|
|
253
|
+
for (const label of labels) {
|
|
254
|
+
let hit = null;
|
|
255
|
+
for (const option of Array.from(field.querySelectorAll('.question-option'))) {
|
|
256
|
+
const text = option.querySelector('.question-option-label');
|
|
257
|
+
if (((text && text.textContent) || '').trim() === label) { hit = option; break; }
|
|
258
|
+
}
|
|
259
|
+
if (!hit) return { clicked: false, reason: 'no option with label ' + label };
|
|
260
|
+
const input = hit.querySelector('input');
|
|
261
|
+
if (!input) return { clicked: false, reason: 'the option has no input' };
|
|
262
|
+
input.click();
|
|
263
|
+
picked.push({ label, checked: input.checked === true });
|
|
264
|
+
}
|
|
265
|
+
return { clicked: true, picked };
|
|
266
|
+
})()`);
|
|
267
|
+
if (!result.clicked) throw new Error(`the checkbox options could not be selected: ${result.reason || 'unknown'}`);
|
|
268
|
+
return result;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// Type a custom answer into the product's own custom input of a question.
|
|
272
|
+
async function typeCustomAnswer(browser, requestId, fieldIndex, text) {
|
|
273
|
+
const result = await browser.evaluate(`(() => {
|
|
274
|
+
const id = ${JSON.stringify(requestId)};
|
|
275
|
+
const index = ${Number(fieldIndex)};
|
|
276
|
+
const value = ${JSON.stringify(text)};
|
|
277
|
+
const node = document.querySelector('.perm.question-card[data-perm-id="' + id.replace(/"/g, '\\\\"') + '"]');
|
|
278
|
+
if (!node) return { typed: false, reason: 'no such question card' };
|
|
279
|
+
const field = node.querySelectorAll('.question-field')[index];
|
|
280
|
+
if (!field) return { typed: false, reason: 'no such question field' };
|
|
281
|
+
const custom = field.querySelector('input.question-custom');
|
|
282
|
+
if (!custom) return { typed: false, reason: 'the question has no custom input' };
|
|
283
|
+
custom.focus();
|
|
284
|
+
custom.value = value;
|
|
285
|
+
custom.dispatchEvent(new Event('input', { bubbles: true }));
|
|
286
|
+
return { typed: true, value: custom.value, type: custom.type };
|
|
287
|
+
})()`);
|
|
288
|
+
if (!result.typed) throw new Error(`the custom answer could not be typed: ${result.reason || 'unknown'}`);
|
|
289
|
+
return result;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// The product's own 提交答案 button.
|
|
293
|
+
async function clickSubmitAnswer(browser, requestId) {
|
|
294
|
+
const result = await browser.evaluate(`(() => {
|
|
295
|
+
const id = ${JSON.stringify(requestId)};
|
|
296
|
+
const node = document.querySelector('.perm.question-card[data-perm-id="' + id.replace(/"/g, '\\\\"') + '"]');
|
|
297
|
+
if (!node) return { clicked: false, reason: 'no such question card' };
|
|
298
|
+
const submit = node.querySelector('.question-submit');
|
|
299
|
+
if (!submit) return { clicked: false, reason: 'the card has no 提交答案 button' };
|
|
300
|
+
const wasDisabled = submit.disabled === true;
|
|
301
|
+
submit.click();
|
|
302
|
+
return { clicked: true, wasDisabled };
|
|
303
|
+
})()`);
|
|
304
|
+
if (!result.clicked) throw new Error(`the 提交答案 button could not be clicked: ${result.reason || 'unknown'}`);
|
|
305
|
+
return result;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// --- the product's own permission-mode menu ---------------------------------
|
|
309
|
+
|
|
310
|
+
// The product renders the permission modes in its own menu (默认防护 /
|
|
311
|
+
// 接受编辑 / 计划模式 / 完全访问). Selecting 完全访问 is the product's own
|
|
312
|
+
// bypass control; the lane uses it to prove that the bypass does not release
|
|
313
|
+
// a pending question.
|
|
314
|
+
async function selectPermissionMode(browser, label) {
|
|
315
|
+
const opened = await browser.evaluate("(() => { const button = document.getElementById('btn-mode'); if (!button) return { opened: false, reason: 'no mode button' }; button.click(); const pop = document.getElementById('pop-mode'); return { opened: Boolean(pop) && pop.hidden === false, items: pop ? Array.from(pop.querySelectorAll('.pop-item')).map(item => ((item.querySelector('.grow') || item).textContent || '').trim()) : [] }; })()");
|
|
316
|
+
if (!opened.opened) throw new Error(`the permission-mode menu did not open: ${opened.reason || 'unknown'}`);
|
|
317
|
+
const clicked = await browser.evaluate(`(() => {
|
|
318
|
+
const label = ${JSON.stringify(label)};
|
|
319
|
+
const pop = document.getElementById('pop-mode');
|
|
320
|
+
for (const item of Array.from(pop.querySelectorAll('.pop-item'))) {
|
|
321
|
+
const text = ((item.querySelector('.grow') || item).textContent || '').trim();
|
|
322
|
+
if (text === label) { item.click(); return { clicked: true, label: text }; }
|
|
323
|
+
}
|
|
324
|
+
return { clicked: false, items: Array.from(pop.querySelectorAll('.pop-item')).map(item => ((item.querySelector('.grow') || item).textContent || '').trim()) };
|
|
325
|
+
})()`);
|
|
326
|
+
if (!clicked.clicked) throw new Error(`the permission mode ${label} is not in the product's menu: ${JSON.stringify(clicked.items)}`);
|
|
327
|
+
return clicked;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
module.exports = {
|
|
331
|
+
wait,
|
|
332
|
+
waitUntil,
|
|
333
|
+
readToolPermissionCards,
|
|
334
|
+
findToolCard,
|
|
335
|
+
readPageStreams,
|
|
336
|
+
readPageEvents,
|
|
337
|
+
readDomDiagnostics,
|
|
338
|
+
clickDeny,
|
|
339
|
+
clickAllow,
|
|
340
|
+
readSettledCard,
|
|
341
|
+
readQuestionCards,
|
|
342
|
+
selectRadioOption,
|
|
343
|
+
selectCheckboxOptions,
|
|
344
|
+
typeCustomAnswer,
|
|
345
|
+
clickSubmitAnswer,
|
|
346
|
+
selectPermissionMode,
|
|
347
|
+
};
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
// P-PROFILE-BLANK-REAL-R49 host — modeled on the tracked
|
|
3
|
+
// helpers/parallel-workbench-drafts-r5-host.js pattern (frozen for R5), with
|
|
4
|
+
// the R49 differences the dispatch requires:
|
|
5
|
+
// * NO ProfileStore seeding at all: every profile and every credential
|
|
6
|
+
// enters through the real HTTP surface of the real module server
|
|
7
|
+
// (normal product flows only);
|
|
8
|
+
// * ephemeral port (listen 0 on 127.0.0.1) — no fixed port is touched;
|
|
9
|
+
// * service-wiring env vars (KB_EVENT_BRIDGE_URL, KB_KNOWLEDGE_ENGINE_URL,
|
|
10
|
+
// KB_TERMINAL_URL, KB_VECTOR_HUB_URL, KB_WORKBENCH_RUNTIME_TOKEN) are
|
|
11
|
+
// scrubbed and asserted absent BEFORE createWorkbenchServer so the module
|
|
12
|
+
// cannot wire itself to any real bridge/engine/vector service even if the
|
|
13
|
+
// ambient environment carried one.
|
|
14
|
+
// The module bytes under test are the root's exact-pinned install
|
|
15
|
+
// (node_modules/claude-ai-workbench@0.13.9), required read-only.
|
|
16
|
+
|
|
17
|
+
const path = require('node:path');
|
|
18
|
+
|
|
19
|
+
const moduleRoot = String(process.env.PK_R49_MODULE_ROOT || '').trim();
|
|
20
|
+
const dataDir = String(process.env.PK_R49_DATA_DIR || '').trim();
|
|
21
|
+
const workspace = String(process.env.PK_R49_WORKSPACE || '').trim();
|
|
22
|
+
|
|
23
|
+
if (!moduleRoot || !dataDir || !workspace) {
|
|
24
|
+
console.error('qa-r49-wb-host: PK_R49_MODULE_ROOT, PK_R49_DATA_DIR and PK_R49_WORKSPACE are required');
|
|
25
|
+
process.exit(2);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
for (const name of ['KB_EVENT_BRIDGE_URL', 'KB_KNOWLEDGE_ENGINE_URL', 'KB_TERMINAL_URL', 'KB_VECTOR_HUB_URL', 'KB_WORKBENCH_RUNTIME_TOKEN', 'KB_ENGINE_URL', 'KB_BRIDGE_HOME']) {
|
|
29
|
+
delete process.env[name];
|
|
30
|
+
}
|
|
31
|
+
for (const name of ['KB_EVENT_BRIDGE_URL', 'KB_KNOWLEDGE_ENGINE_URL', 'KB_TERMINAL_URL', 'KB_VECTOR_HUB_URL', 'KB_WORKBENCH_RUNTIME_TOKEN', 'KB_ENGINE_URL', 'KB_BRIDGE_HOME']) {
|
|
32
|
+
if (process.env[name] !== undefined) throw new Error(`qa-r49-wb-host: ${name} survived scrub`);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Same contract as the module's own bin: the runtime resolves its data
|
|
36
|
+
// directory from these.
|
|
37
|
+
process.env.CLAUDE_AI_WORKBENCH_DATA_DIR = dataDir;
|
|
38
|
+
process.env.KB_DATA_DIR = dataDir;
|
|
39
|
+
|
|
40
|
+
const { createWorkbenchServer } = require(path.join(moduleRoot, 'packages', 'server'));
|
|
41
|
+
|
|
42
|
+
const app = createWorkbenchServer({ dataDir, workspaces: { 'r49-fixture': workspace } });
|
|
43
|
+
app.server.listen(0, '127.0.0.1', () => {
|
|
44
|
+
const address = app.server.address();
|
|
45
|
+
process.stdout.write(`R49_WB_HOST_READY http://127.0.0.1:${address.port} module=${moduleRoot} data=${dataDir}\n`);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
let stopping = false;
|
|
49
|
+
async function shutdown() {
|
|
50
|
+
if (stopping) return;
|
|
51
|
+
stopping = true;
|
|
52
|
+
setTimeout(() => process.exit(1), 10000).unref();
|
|
53
|
+
try { await app.service.destroy(); } catch { process.exitCode = 1; }
|
|
54
|
+
app.server.closeAllConnections?.();
|
|
55
|
+
app.server.close(() => process.exit(process.exitCode || 0));
|
|
56
|
+
}
|
|
57
|
+
process.on('SIGINT', shutdown);
|
|
58
|
+
process.on('SIGTERM', shutdown);
|