@sanqianx/project-knowledge 5.0.20-rc.1 → 5.0.21
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/CHANGELOG.md +6 -1
- package/_site/_test/agent-capture-mcp-ui-test.js +2 -1
- package/_site/_test/agent-capture-ui-test.js +2 -1
- package/_site/_test/knowledge-panel-lifecycle-test.js +154 -0
- package/_site/_test/knowledge-panel-ui-contract-test.js +110 -112
- package/_site/_test/workspace-ui-contract-test.js +31 -31
- package/module-runtime-manifest.json +128 -126
- 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 -1
- package/ui/knowledge-panel.html +11 -14
- package/ui/product.css +2 -10
- package/ui/product.js +73 -56
package/CHANGELOG.md
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
## [
|
|
3
|
+
## [5.0.21] - 2026-10-20
|
|
4
|
+
|
|
5
|
+
- 开始工作采用并排工作区:从加号新建子代理或按需添加知识引用,使用统一的对话、工具步骤、文件修改和输入框;移除面向客户的任务标识、派发阶段及诊断信息。
|
|
6
|
+
- 子代理首次发送时继承当前项目、Agent、模型及主对话的冻结上下文,之后独立交流;原生任务可从主对话打开,并基于结果继续创建子对话。
|
|
7
|
+
- 知识引用仅保留会话内单一标签,检索不自动展开、不抢焦点;修复刷新时内容提前投递的问题,并跟随产品深浅主题。
|
|
8
|
+
- 主子草稿、标签和阅读位置可恢复;关闭标签不停止任务,窄窗口自动使用抽屉。精确锁定 Workbench 0.13.3。
|
|
4
9
|
|
|
5
10
|
- 默认维护模型明确报告额度不足时,保留并展示可重试的失败原因;请求限流、认证失败分别展示。同项目后续提交等待前一条恢复,失败及排队记录跨重启保留。
|
|
6
11
|
- 事件消费在分析任务建单前受阻时,活动页与系统状态展示故障,不再只显示空记录;故障及恢复通过现有实时连接更新,稳定状态不触发额外数据轮询。
|
|
@@ -58,10 +58,11 @@ async function main() {
|
|
|
58
58
|
assert.equal(browser.exceptions.length, 0, JSON.stringify(browser.exceptions));
|
|
59
59
|
console.log('agent-capture-mcp-ui-test PASS (MCP line rendered separately; hostile detail escaped; no XSS execution)');
|
|
60
60
|
} finally {
|
|
61
|
+
runtime.integrationStatusScan?.close();
|
|
61
62
|
await browser?.close(); server.closeAllConnections(); await new Promise(resolve => server.close(resolve));
|
|
62
63
|
offline.closeAllConnections(); await new Promise(resolve => offline.close(resolve)); await runtime.logger.close();
|
|
63
64
|
assert.equal(path.dirname(temp), path.resolve(os.tmpdir())); assert.ok(path.basename(temp).startsWith('kb-agent-mcp-ui-'));
|
|
64
|
-
fs.
|
|
65
|
+
await fs.promises.rm(temp, { recursive: true, force: true, maxRetries: 12, retryDelay: 200 });
|
|
65
66
|
}
|
|
66
67
|
}
|
|
67
68
|
main().catch(error => { console.error(error.stack); process.exitCode = 1; });
|
|
@@ -91,10 +91,11 @@ async function main() {
|
|
|
91
91
|
assert.equal(browser.exceptions.length, 0, JSON.stringify(browser.exceptions));
|
|
92
92
|
console.log(`agent-capture-ui-test PASS (1920x1080; single-switch install/repair/Hook/journal/error logs/intent/removal/light+dark; core=${coreRoot})`);
|
|
93
93
|
} finally {
|
|
94
|
+
runtime.integrationStatusScan?.close();
|
|
94
95
|
await browser?.close(); server.closeAllConnections(); await new Promise(resolve => server.close(resolve));
|
|
95
96
|
offline.closeAllConnections(); await new Promise(resolve => offline.close(resolve)); await runtime.logger.close();
|
|
96
97
|
assert.equal(path.dirname(temp), path.resolve(os.tmpdir())); assert.ok(path.basename(temp).startsWith('kb-agent-capture-ui-'));
|
|
97
|
-
fs.
|
|
98
|
+
await fs.promises.rm(temp, { recursive: true, force: true, maxRetries: 12, retryDelay: 200 });
|
|
98
99
|
}
|
|
99
100
|
}
|
|
100
101
|
main().catch(error => { console.error(error.stack); process.exitCode = 1; });
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
const test = require('node:test');
|
|
3
|
+
const assert = require('node:assert/strict');
|
|
4
|
+
const fs = require('node:fs');
|
|
5
|
+
const path = require('node:path');
|
|
6
|
+
const vm = require('node:vm');
|
|
7
|
+
const source = fs.readFileSync(path.join(__dirname, '../../ui/product.js'), 'utf8');
|
|
8
|
+
const lifecycle = source.slice(source.indexOf(' function knowledgePanelFailed('), source.indexOf(' function panelRequest('))
|
|
9
|
+
+ source.slice(source.indexOf(' async function resetKnowledgePanel('), source.indexOf(' function setView('));
|
|
10
|
+
const settle = async () => { for (let i = 0; i < 5; i++) await new Promise(resolve => setImmediate(resolve)); };
|
|
11
|
+
function harness(options = {}) {
|
|
12
|
+
const calls = [], notices = [], timers = new Map();
|
|
13
|
+
const panel = { providerId: 'knowledge', registered: true, registering: false, failed: false, error: null, panelId: '', version: 0, pending: null, sending: null, queued: false, deliveredPanelId: '' };
|
|
14
|
+
const state = { projectId: 'project-a', epoch: 0 };
|
|
15
|
+
let serial = 0, context;
|
|
16
|
+
const deliver = (id, content = null, error = null) => context.handlePanelState({ panels: id ? [{ providerId: 'knowledge', panelId: id, content, error }] : [] });
|
|
17
|
+
context = vm.createContext({
|
|
18
|
+
knowledgePanel: panel, state, frames: { terminal: { ready: true } },
|
|
19
|
+
knowledgePayload: id => ({ projectId: id, references: [{ title: state.reference || id + '-fact' }] }),
|
|
20
|
+
toast: text => notices.push(text),
|
|
21
|
+
setTimeout(fn) { timers.set(++serial, fn); return serial; }, clearTimeout: id => timers.delete(id),
|
|
22
|
+
panelRequest: async (type, payload) => {
|
|
23
|
+
calls.push({ type, payload });
|
|
24
|
+
if (options.request) {
|
|
25
|
+
const response = options.request(type, payload, { deliver, panel, state });
|
|
26
|
+
if (response !== undefined) return response;
|
|
27
|
+
}
|
|
28
|
+
if (type === 'kb:panel/register') return { ok: true };
|
|
29
|
+
if (type === 'kb:panel/content') {
|
|
30
|
+
deliver(payload.panelId, { ack: { version: payload.version, ok: true } });
|
|
31
|
+
return { ok: true };
|
|
32
|
+
}
|
|
33
|
+
throw new Error('Knowledge delivery cannot open, activate or unregister a view: ' + type);
|
|
34
|
+
},
|
|
35
|
+
});
|
|
36
|
+
vm.runInContext(lifecycle, context);
|
|
37
|
+
const show = async id => { deliver(id); await settle(); };
|
|
38
|
+
return { context, panel, state, calls, notices, timers, deliver, show };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
test('startup registration and cached updates never create a knowledge tab', async () => {
|
|
42
|
+
const h = harness();
|
|
43
|
+
await h.context.resetKnowledgePanel();
|
|
44
|
+
assert.equal(h.panel.registered, true);
|
|
45
|
+
assert.equal(h.panel.panelId, '');
|
|
46
|
+
await h.context.pushKnowledgeToPanel();
|
|
47
|
+
h.state.projectId = 'project-b'; h.state.epoch++;
|
|
48
|
+
await h.context.pushKnowledgeToPanel();
|
|
49
|
+
assert.deepEqual(h.calls.map(call => call.type), ['kb:panel/register']);
|
|
50
|
+
assert.equal(h.timers.size, 0);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test('user-added tabs receive content and fast acknowledgements without opening another view', async () => {
|
|
54
|
+
const h = harness();
|
|
55
|
+
await h.show('tab-1');
|
|
56
|
+
assert.equal(h.panel.pending, null);
|
|
57
|
+
assert.equal(h.panel.deliveredPanelId, 'tab-1');
|
|
58
|
+
assert.equal(h.timers.size, 0);
|
|
59
|
+
assert.deepEqual(h.calls.map(call => call.type), ['kb:panel/content']);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test('closing keeps cached updates invisible until the user adds the page again', async () => {
|
|
63
|
+
const h = harness();
|
|
64
|
+
await h.show('tab-1');
|
|
65
|
+
h.deliver(null);
|
|
66
|
+
h.state.reference = 'updated while closed';
|
|
67
|
+
await h.context.pushKnowledgeToPanel();
|
|
68
|
+
assert.equal(h.panel.panelId, '');
|
|
69
|
+
assert.equal(h.calls.length, 1, 'closing or receiving knowledge must not create another view');
|
|
70
|
+
await h.show('tab-2');
|
|
71
|
+
assert.equal(h.calls.length, 2);
|
|
72
|
+
assert.equal(h.calls[1].payload.panelId, 'tab-2');
|
|
73
|
+
assert.equal(h.calls[1].payload.content.references[0].title, 'updated while closed');
|
|
74
|
+
assert.equal(h.panel.deliveredPanelId, 'tab-2');
|
|
75
|
+
h.deliver('tab-2', { ack: { version: h.panel.version, ok: true } });
|
|
76
|
+
await settle();
|
|
77
|
+
assert.equal(h.calls.length, 2, 'focus and repeated receipts cannot create a delivery loop');
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
test('errors never create an outside fallback and a newly added page can retry', async () => {
|
|
81
|
+
let first = true;
|
|
82
|
+
const h = harness({ request(type) {
|
|
83
|
+
if (type === 'kb:panel/content' && first) { first = false; return { ok: false }; }
|
|
84
|
+
} });
|
|
85
|
+
await h.show('tab-1');
|
|
86
|
+
assert.equal(h.panel.failed, true);
|
|
87
|
+
assert.equal(h.panel.registered, true, 'the page remains available in the dock selector');
|
|
88
|
+
assert.match(h.notices.join(' '), /关闭引用标签/);
|
|
89
|
+
assert.deepEqual(h.calls.map(call => call.type), ['kb:panel/content']);
|
|
90
|
+
h.deliver(null);
|
|
91
|
+
await h.show('tab-2');
|
|
92
|
+
assert.equal(h.panel.failed, false);
|
|
93
|
+
assert.equal(h.panel.deliveredPanelId, 'tab-2');
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
test('closing during a content transfer makes late replies inert', async () => {
|
|
97
|
+
let reply;
|
|
98
|
+
const h = harness({ request(type) {
|
|
99
|
+
if (type === 'kb:panel/content') return new Promise(resolve => { reply = resolve; });
|
|
100
|
+
} });
|
|
101
|
+
await h.show('tab-1');
|
|
102
|
+
h.deliver(null);
|
|
103
|
+
reply({ ok: true });
|
|
104
|
+
await settle();
|
|
105
|
+
assert.equal(h.panel.panelId, '');
|
|
106
|
+
assert.equal(h.panel.pending, null);
|
|
107
|
+
assert.equal(h.timers.size, 0);
|
|
108
|
+
assert.equal(h.calls.length, 1);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
test('an older failed acknowledgement cannot close the current reference view', async () => {
|
|
112
|
+
const h = harness({ request(type) { if (type === 'kb:panel/content') return { ok: true }; } });
|
|
113
|
+
await h.show('tab-1');
|
|
114
|
+
await h.context.pushKnowledgeToPanel();
|
|
115
|
+
const version = h.panel.pending.version;
|
|
116
|
+
h.deliver('tab-1', { ack: { version: version - 1, ok: false } }, 'CONTENT_ACK_ERROR');
|
|
117
|
+
await settle();
|
|
118
|
+
assert.equal(h.panel.pending.version, version);
|
|
119
|
+
assert.equal(h.panel.failed, false);
|
|
120
|
+
assert.equal(h.calls.length, 2);
|
|
121
|
+
h.deliver('tab-1', { ack: { version, ok: true } });
|
|
122
|
+
assert.equal(h.panel.deliveredPanelId, 'tab-1');
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
test('a queued project switch sends only the latest scope and rejects a stale acknowledgement', async () => {
|
|
126
|
+
let oldReply;
|
|
127
|
+
const h = harness({ request(type, payload) {
|
|
128
|
+
if (type === 'kb:panel/content' && payload.content.projectId === 'project-a') return new Promise(resolve => { oldReply = resolve; });
|
|
129
|
+
} });
|
|
130
|
+
await h.show('tab-1');
|
|
131
|
+
h.state.projectId = 'project-b'; h.state.epoch++;
|
|
132
|
+
await h.context.pushKnowledgeToPanel();
|
|
133
|
+
h.deliver('tab-1', { ack: { version: 1, ok: true } });
|
|
134
|
+
assert.equal(h.panel.deliveredPanelId, '');
|
|
135
|
+
oldReply({ ok: true });
|
|
136
|
+
await settle();
|
|
137
|
+
assert.deepEqual(h.calls.map(call => call.payload.content.projectId), ['project-a', 'project-b']);
|
|
138
|
+
assert.equal(h.panel.deliveredPanelId, 'tab-1');
|
|
139
|
+
assert.equal(h.panel.pending, null);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
test('restored inactive reference tabs wait for their page to load before content delivery', async () => {
|
|
143
|
+
const h = harness();
|
|
144
|
+
await h.context.resetKnowledgePanel();
|
|
145
|
+
h.context.handlePanelState({ panels: [{ providerId: 'knowledge', panelId: 'restored', ready: false }] });
|
|
146
|
+
await h.context.pushKnowledgeToPanel(); await settle();
|
|
147
|
+
assert.deepEqual(h.calls.map(call => call.type), ['kb:panel/register']);
|
|
148
|
+
assert.equal(h.panel.failed, false);
|
|
149
|
+
assert.equal(h.timers.size, 0);
|
|
150
|
+
h.context.handlePanelState({ panels: [{ providerId: 'knowledge', panelId: 'restored', ready: true }] });
|
|
151
|
+
await settle();
|
|
152
|
+
assert.deepEqual(h.calls.map(call => call.type), ['kb:panel/register', 'kb:panel/content']);
|
|
153
|
+
assert.equal(h.panel.deliveredPanelId, 'restored');
|
|
154
|
+
});
|
|
@@ -1,112 +1,110 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
// K1 knowledge panel contract: the shell registers a same-origin provider with
|
|
3
|
-
// the workbench, streams structured references
|
|
4
|
-
//
|
|
5
|
-
// page itself renders external strings through textContent only.
|
|
6
|
-
const assert = require('node:assert/strict');
|
|
7
|
-
const fs = require('node:fs');
|
|
8
|
-
const path = require('node:path');
|
|
9
|
-
const root = path.resolve(__dirname, '../..');
|
|
10
|
-
const html = fs.readFileSync(path.join(root, 'ui/index.html'), 'utf8');
|
|
11
|
-
const script = fs.readFileSync(path.join(root, 'ui/product.js'), 'utf8');
|
|
12
|
-
const css = fs.readFileSync(path.join(root, 'ui/product.css'), 'utf8');
|
|
13
|
-
const panelPath = path.join(root, 'ui/knowledge-panel.html');
|
|
14
|
-
const panelHtml = fs.readFileSync(panelPath, 'utf8');
|
|
15
|
-
|
|
16
|
-
/* ===================== panel page source contract ===================== */
|
|
17
|
-
assert.ok(panelHtml.includes('KB-K1-KNOWLEDGE-PANEL-v1'), '
|
|
18
|
-
assert.ok(panelHtml.includes('event.source !== window.parent'), 'the panel only accepts its Workbench parent as the message source');
|
|
19
|
-
assert.ok(panelHtml.includes("data.type !== 'kb:panel/content'"), 'kb:panel/content is the only inbound verb');
|
|
20
|
-
assert.ok(panelHtml.includes("'kb:panel/content-ack'"), 'the panel answers with a content ack');
|
|
21
|
-
assert.ok(!panelHtml.includes('innerHTML') && !panelHtml.includes('insertAdjacentHTML') && !panelHtml.includes('document.write'), 'no string becomes markup');
|
|
22
|
-
assert.ok(!/<(?:script|link|img)[^>]+(?:src|href)=/.test(panelHtml.replace(/<script>[\s\S]*?<\/script>/, '')), 'the panel page loads no external resources');
|
|
23
|
-
assert.ok(!panelHtml.includes('fetch(') && !panelHtml.includes('XMLHttpRequest'), 'the sandboxed panel never talks to APIs directly');
|
|
24
|
-
const panelScriptMatch = panelHtml.match(/<script>([\s\S]*?)<\/script>/);
|
|
25
|
-
assert.ok(panelScriptMatch, 'the panel page carries an inline script');
|
|
26
|
-
assert.doesNotThrow(() => new Function(panelScriptMatch[1]));
|
|
27
|
-
|
|
28
|
-
/* ===================== panel page behavior (minimal DOM) ===================== */
|
|
29
|
-
function runPanelPage() {
|
|
30
|
-
const acks = [];
|
|
31
|
-
const node = () => ({ children: [], textContent: '', append(...items) { this.children.push(...items); }, replaceChildren() { this.children.length = 0; } });
|
|
32
|
-
const elements = { panelTitle: node(), panelProject: node(), panelStatus: node(), panelReferences: node(), panelMarker: node() };
|
|
33
|
-
let listener = null;
|
|
34
|
-
const parent = { postMessage(message) { acks.push(message); } };
|
|
35
|
-
const fakeWindow = { parent, addEventListener(type, fn) { if (type === 'message') listener = fn; } };
|
|
36
|
-
const fakeDocument = {
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
page
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
assert.equal(ack.
|
|
55
|
-
assert.equal(ack.
|
|
56
|
-
assert.equal(ack.
|
|
57
|
-
assert.equal(
|
|
58
|
-
assert.equal(page.elements.
|
|
59
|
-
|
|
60
|
-
assert.
|
|
61
|
-
const
|
|
62
|
-
assert.ok(
|
|
63
|
-
assert.equal(
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
page.
|
|
73
|
-
|
|
74
|
-
assert.equal(
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
assert.
|
|
83
|
-
assert.match(script, /
|
|
84
|
-
assert.match(script, /
|
|
85
|
-
assert.
|
|
86
|
-
assert.match(script, /
|
|
87
|
-
assert.match(script, /
|
|
88
|
-
assert.
|
|
89
|
-
assert.
|
|
90
|
-
assert.match(script, /
|
|
91
|
-
assert.match(script, /
|
|
92
|
-
assert.match(script, /
|
|
93
|
-
assert.match(script,
|
|
94
|
-
assert.match(script, /
|
|
95
|
-
assert.
|
|
96
|
-
assert.
|
|
97
|
-
assert.
|
|
98
|
-
assert.match(script, /
|
|
99
|
-
assert.
|
|
100
|
-
assert.match(script, /
|
|
101
|
-
assert.
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
assert.ok(
|
|
107
|
-
assert.
|
|
108
|
-
assert.match(css,
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
console.log('knowledge panel UI contract test PASS (source contract + sandboxed render behavior; not browser acceptance)');
|
|
1
|
+
'use strict';
|
|
2
|
+
// K1 knowledge panel contract: the shell registers a same-origin provider with
|
|
3
|
+
// the workbench, streams structured references only to user-added tabs,
|
|
4
|
+
// validates matching receipts, never renders an outside column, and the panel
|
|
5
|
+
// page itself renders external strings through textContent only.
|
|
6
|
+
const assert = require('node:assert/strict');
|
|
7
|
+
const fs = require('node:fs');
|
|
8
|
+
const path = require('node:path');
|
|
9
|
+
const root = path.resolve(__dirname, '../..');
|
|
10
|
+
const html = fs.readFileSync(path.join(root, 'ui/index.html'), 'utf8');
|
|
11
|
+
const script = fs.readFileSync(path.join(root, 'ui/product.js'), 'utf8');
|
|
12
|
+
const css = fs.readFileSync(path.join(root, 'ui/product.css'), 'utf8');
|
|
13
|
+
const panelPath = path.join(root, 'ui/knowledge-panel.html');
|
|
14
|
+
const panelHtml = fs.readFileSync(panelPath, 'utf8');
|
|
15
|
+
|
|
16
|
+
/* ===================== panel page source contract ===================== */
|
|
17
|
+
assert.ok(!panelHtml.includes('KB-K1-KNOWLEDGE-PANEL-v1'), 'test contract markers never enter customer markup');
|
|
18
|
+
assert.ok(panelHtml.includes('event.source !== window.parent'), 'the panel only accepts its Workbench parent as the message source');
|
|
19
|
+
assert.ok(panelHtml.includes("data.type !== 'kb:panel/content'"), 'kb:panel/content is the only inbound verb');
|
|
20
|
+
assert.ok(panelHtml.includes("'kb:panel/content-ack'"), 'the panel answers with a content ack');
|
|
21
|
+
assert.ok(!panelHtml.includes('innerHTML') && !panelHtml.includes('insertAdjacentHTML') && !panelHtml.includes('document.write'), 'no string becomes markup');
|
|
22
|
+
assert.ok(!/<(?:script|link|img)[^>]+(?:src|href)=/.test(panelHtml.replace(/<script>[\s\S]*?<\/script>/, '')), 'the panel page loads no external resources');
|
|
23
|
+
assert.ok(!panelHtml.includes('fetch(') && !panelHtml.includes('XMLHttpRequest'), 'the sandboxed panel never talks to APIs directly');
|
|
24
|
+
const panelScriptMatch = panelHtml.match(/<script>([\s\S]*?)<\/script>/);
|
|
25
|
+
assert.ok(panelScriptMatch, 'the panel page carries an inline script');
|
|
26
|
+
assert.doesNotThrow(() => new Function(panelScriptMatch[1]));
|
|
27
|
+
|
|
28
|
+
/* ===================== panel page behavior (minimal DOM) ===================== */
|
|
29
|
+
function runPanelPage() {
|
|
30
|
+
const acks = [];
|
|
31
|
+
const node = () => ({ children: [], textContent: '', append(...items) { this.children.push(...items); }, replaceChildren() { this.children.length = 0; } });
|
|
32
|
+
const elements = { panelTitle: node(), panelProject: node(), panelStatus: node(), panelReferences: node(), panelMarker: node() };
|
|
33
|
+
let listener = null;
|
|
34
|
+
const parent = { postMessage(message) { acks.push(message); } };
|
|
35
|
+
const fakeWindow = { parent, addEventListener(type, fn) { if (type === 'message') listener = fn; } };
|
|
36
|
+
const fakeDocument = {
|
|
37
|
+
documentElement: { dataset: {} },
|
|
38
|
+
getElementById: id => elements[id] || null,
|
|
39
|
+
createElement: () => node(),
|
|
40
|
+
};
|
|
41
|
+
new Function('window', 'document', panelScriptMatch[1])(fakeWindow, fakeDocument);
|
|
42
|
+
assert.equal(typeof listener, 'function', 'the panel registers a message listener');
|
|
43
|
+
const deliver = (data, source = parent) => listener({ source, data });
|
|
44
|
+
return { acks, elements, deliver, document: fakeDocument };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
{
|
|
48
|
+
const page = runPanelPage();
|
|
49
|
+
page.deliver({ type: 'kb:panel/content', panelId: 'tab-1', version: 3, content: {
|
|
50
|
+
title: '本次知识引用', projectId: 'proj-a', projectName: '项目 A', status: 'ready', theme: 'dark',
|
|
51
|
+
references: [{ entryId: 'docs/a.md', title: 'T', text: '<img src=x onerror=alert(1)>', source: 'proj / docs/a.md' }],
|
|
52
|
+
} });
|
|
53
|
+
const ack = page.acks[page.acks.length - 1];
|
|
54
|
+
assert.equal(ack.type, 'kb:panel/content-ack');
|
|
55
|
+
assert.equal(ack.panelId, 'tab-1');
|
|
56
|
+
assert.equal(ack.version, 3);
|
|
57
|
+
assert.equal(ack.ok, true);
|
|
58
|
+
assert.equal(page.elements.panelTitle.textContent, '本次知识引用');
|
|
59
|
+
assert.equal(page.elements.panelProject.textContent, '项目 A');
|
|
60
|
+
assert.equal(page.document.documentElement.dataset.theme, 'dark', 'the reference page follows the host theme');
|
|
61
|
+
const reference = page.elements.panelReferences.children[0];
|
|
62
|
+
assert.ok(reference, 'one reference card rendered');
|
|
63
|
+
assert.equal(reference.children.length, 3, 'reference cards show title, source and excerpt without a repeated internal entry identifier');
|
|
64
|
+
const text = reference.children.find(child => child.textContent.includes('<img'));
|
|
65
|
+
assert.ok(text, 'the reference body exists');
|
|
66
|
+
assert.equal(text.textContent, '<img src=x onerror=alert(1)>', 'markup-looking strings stay inert text');
|
|
67
|
+
}
|
|
68
|
+
{
|
|
69
|
+
const page = runPanelPage();
|
|
70
|
+
const stranger = { postMessage() {} };
|
|
71
|
+
page.deliver({ type: 'kb:panel/content', panelId: 'tab-1', version: 1, content: { title: 'x' } }, stranger);
|
|
72
|
+
assert.equal(page.acks.length, 0, 'a non-parent source is ignored without any ack');
|
|
73
|
+
page.deliver({ type: 'kb:panel/command', panelId: 'tab-1' });
|
|
74
|
+
assert.equal(page.acks.length, 0, 'non-content verbs are ignored');
|
|
75
|
+
page.deliver({ type: 'kb:panel/content', panelId: 'tab-1', version: 2, content: ['not', 'an', 'object'] });
|
|
76
|
+
const failure = page.acks[page.acks.length - 1];
|
|
77
|
+
assert.equal(failure.ok, false, 'illegal content is refused with an explicit receipt');
|
|
78
|
+
assert.ok(failure.error, 'the refusal carries an error message');
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/* ===================== shell (product.js) contract ===================== */
|
|
82
|
+
assert.ok(script.includes('kb.knowledge.references'), 'the provider id is stable');
|
|
83
|
+
assert.match(script, /url: '\/knowledge-panel\.html'/, 'the provider URL is the KB-owned panel page');
|
|
84
|
+
assert.match(script, /panelType: 'knowledge', title: '本次知识引用'/, 'panel type and title match the product');
|
|
85
|
+
assert.match(script, /capabilities: \['read-only'\]/, 'the panel declares itself read-only');
|
|
86
|
+
assert.match(script, /multiInstance: 'single'/, 'the provider is single-instance');
|
|
87
|
+
assert.match(script, /if \(message\.type === 'kb:ready'\)[\s\S]*?if \(key === 'terminal'\) void resetKnowledgePanel\(\)/, 'registration starts from the terminal kb:ready');
|
|
88
|
+
assert.ok(script.includes("'kb:panel/register'") && script.includes("'kb:panel/content'"), 'the host registers the page and supplies its content');
|
|
89
|
+
assert.ok(!script.includes("'kb:panel/open'"), 'registration and retrieval cannot open a page');
|
|
90
|
+
assert.match(script, /if \(message\.type === 'kb:knowledge' && key === 'terminal'\)[\s\S]*?state\.contexts\.set[\s\S]*?void pushKnowledgeToPanel/, 'retrieved knowledge stays cached and updates existing pages only');
|
|
91
|
+
assert.match(script, /const ack = panel\.content\?\.ack;[\s\S]*?if \(ack && String\(ack\.version\) === String\(pending\.version\)\)/, 'receipts match the currently pending content');
|
|
92
|
+
assert.match(script, /if \(ack\.ok && !panel\.error\) \{ knowledgePanel\.deliveredPanelId = panel\.panelId;/, 'delivery is confirmed only by an error-free acknowledgement');
|
|
93
|
+
assert.match(script, /if \(!panel\) \{[\s\S]*?knowledgePanel\.panelId = '';[\s\S]*?return;/, 'closing a page cancels its delivery');
|
|
94
|
+
assert.match(script, /if \(!knowledgePanel\.registered \|\| knowledgePanel\.failed \|\| !projectId \|\| !panelId\) return;/, 'closed pages cannot be opened by background updates');
|
|
95
|
+
assert.ok(!/setKnowledgeCollapsed|panelNotice|contextContent|knowledge-context/.test(script), 'the old outside-column paths are removed');
|
|
96
|
+
assert.match(script, /\.slice\(0, 50\)\.map\(r => \(\{/, 'the payload bounds the reference count');
|
|
97
|
+
assert.match(script, /text: String\(r\.text \|\| ''\)\.slice\(0, 800\)/, 'the payload bounds reference text');
|
|
98
|
+
assert.match(script, /knowledgePayload\(projectId\)[\s\S]*?entryId: String\(/, 'payload fields are coerced to plain strings');
|
|
99
|
+
assert.ok(!/data-action="collapse|toggle-context|折叠知识栏/.test(script + html), 'no duplicate column control button is introduced');
|
|
100
|
+
assert.match(script, /panelRequest\('kb:panel\/content', \{ panelId: knowledgePanel\.panelId, providerId: knowledgePanel\.providerId, version, content: knowledgePayload\(projectId\) \}\)/, 'content carries panelId, providerId and version');
|
|
101
|
+
assert.match(script, /message\.type\.startsWith\('kb:panel\/'\)/, 'panel replies are routed before the frame-context staleness drop');
|
|
102
|
+
assert.match(script, /if \(message\.requestId && message\.requestId !== frames\[key\]\.requestId\) return;/, 'the legacy staleness drop stays intact for module traffic');
|
|
103
|
+
assert.doesNotThrow(() => new Function(script));
|
|
104
|
+
|
|
105
|
+
/* ===================== markup and css contract ===================== */
|
|
106
|
+
assert.ok(!/knowledge-context|contextContent|contextScope|panelNotice/.test(html), 'there is no outside knowledge column, including hidden markup');
|
|
107
|
+
assert.ok(!/knowledge-context|kb-panel-collapsed|panel-notice/.test(css), 'no space or fallback layout is reserved for a second column');
|
|
108
|
+
assert.match(css, /\.work-layout\{display:grid;grid-template-columns:minmax\(0,1fr\);/, 'the host gives the whole workspace to the conversation and its dock');
|
|
109
|
+
|
|
110
|
+
console.log('knowledge panel UI contract test PASS (source contract + sandboxed render behavior; not browser acceptance)');
|
|
@@ -1,31 +1,31 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
const assert = require('node:assert/strict');
|
|
3
|
-
const fs = require('node:fs');
|
|
4
|
-
const path = require('node:path');
|
|
5
|
-
const root = path.resolve(__dirname, '../..');
|
|
6
|
-
const html = fs.readFileSync(path.join(root, 'ui/index.html'), 'utf8');
|
|
7
|
-
const script = fs.readFileSync(path.join(root, 'ui/product.js'), 'utf8');
|
|
8
|
-
const css = fs.readFileSync(path.join(root, 'ui/product.css'), 'utf8');
|
|
9
|
-
for (const view of ['home', 'work', 'search', 'memory', 'projects', 'activity']) {
|
|
10
|
-
assert.equal((html.match(new RegExp(`data-view="${view}"`, 'g')) || []).length, 1);
|
|
11
|
-
assert.ok(html.includes(`id="view-${view}"`));
|
|
12
|
-
}
|
|
13
|
-
assert.equal((html.match(/data-view=/g) || []).length, 6, 'diagnostics must not become another primary product');
|
|
14
|
-
assert.match(html, /<select\b[^>]*id="projectSelect"[^>]*\bdisabled\b/, 'project selection is disabled until the initial registry snapshot has loaded');
|
|
15
|
-
for (const id of ['workFrame', 'vectorFrame', 'bridgeFrame', 'engineFrame', 'importDialog', 'importCode', 'importKnowledge', 'importDomain', 'setupImportRoot', 'createDomain', 'logsDialog', 'systemDrawer', 'copyReference']) assert.equal((html.match(new RegExp(`id="${id}"`, 'g')) || []).length, 1);
|
|
16
|
-
assert.ok(html.includes('data-action="import"') && html.includes('不分析全部历史'));
|
|
17
|
-
assert.ok(!html.includes('srcdoc') && !/data-simulate|data-analyze|data-install-hook/.test(html));
|
|
18
|
-
assert.equal((script.match(/\.src = /g) || []).length, 1, 'frames initialize once, not on project/theme/page changes');
|
|
19
|
-
for (const contract of ['kb:theme', 'kb:select-project', 'kb:scope', 'kb:knowledge', 'kb:ready']) assert.ok(script.includes(contract));
|
|
20
|
-
assert.ok(script.includes('event.origin !== location.origin') && script.includes('contentWindow === event.source'), 'module messages must be from the expected frame and origin');
|
|
21
|
-
assert.ok(script.includes('state.epoch++') && script.includes('referenceEpoch') && script.includes('logEpoch'), 'late project/reference/log feedback must be isolated');
|
|
22
|
-
assert.match(script, /on\('domainForm'[\s\S]*?await refreshRegistry\(true\)/, 'domain writes must invalidate pre-mutation reads');
|
|
23
|
-
assert.match(script, /registered = true;[\s\S]*?await refreshRegistry\(true\)/, 'import must not reuse a pre-import snapshot');
|
|
24
|
-
assert.ok(script.includes(
|
|
25
|
-
assert.ok(script.includes('text.textContent = record.message') && script.includes("'logs/appended'"), 'diagnostics stay inert and stream live');
|
|
26
|
-
assert.ok(script.includes('URL.revokeObjectURL(url)') && script.includes('logStream?.close()'));
|
|
27
|
-
assert.ok(script.includes('resumeImport = true') && script.includes("$('importDialog').showModal()") && script.includes("p.code === 'KNOWLEDGE_ROOT_MISSING'"), 'first import can return from setup without resetting its input');
|
|
28
|
-
assert.ok(css.includes('height:100%') && css.includes('.view.module-view{padding:0;overflow:hidden}') && css.includes('overflow-wrap:anywhere'));
|
|
29
|
-
assert.ok(css.includes('html[data-theme=dark]') && css.includes('prefers-reduced-motion'));
|
|
30
|
-
assert.doesNotThrow(() => new Function(script));
|
|
31
|
-
console.log('workspace UI contract test PASS (source contract; not browser acceptance)');
|
|
1
|
+
'use strict';
|
|
2
|
+
const assert = require('node:assert/strict');
|
|
3
|
+
const fs = require('node:fs');
|
|
4
|
+
const path = require('node:path');
|
|
5
|
+
const root = path.resolve(__dirname, '../..');
|
|
6
|
+
const html = fs.readFileSync(path.join(root, 'ui/index.html'), 'utf8');
|
|
7
|
+
const script = fs.readFileSync(path.join(root, 'ui/product.js'), 'utf8');
|
|
8
|
+
const css = fs.readFileSync(path.join(root, 'ui/product.css'), 'utf8');
|
|
9
|
+
for (const view of ['home', 'work', 'search', 'memory', 'projects', 'activity']) {
|
|
10
|
+
assert.equal((html.match(new RegExp(`data-view="${view}"`, 'g')) || []).length, 1);
|
|
11
|
+
assert.ok(html.includes(`id="view-${view}"`));
|
|
12
|
+
}
|
|
13
|
+
assert.equal((html.match(/data-view=/g) || []).length, 6, 'diagnostics must not become another primary product');
|
|
14
|
+
assert.match(html, /<select\b[^>]*id="projectSelect"[^>]*\bdisabled\b/, 'project selection is disabled until the initial registry snapshot has loaded');
|
|
15
|
+
for (const id of ['workFrame', 'vectorFrame', 'bridgeFrame', 'engineFrame', 'importDialog', 'importCode', 'importKnowledge', 'importDomain', 'setupImportRoot', 'createDomain', 'logsDialog', 'systemDrawer', 'copyReference']) assert.equal((html.match(new RegExp(`id="${id}"`, 'g')) || []).length, 1);
|
|
16
|
+
assert.ok(html.includes('data-action="import"') && html.includes('不分析全部历史'));
|
|
17
|
+
assert.ok(!html.includes('srcdoc') && !/data-simulate|data-analyze|data-install-hook/.test(html));
|
|
18
|
+
assert.equal((script.match(/\.src = /g) || []).length, 1, 'frames initialize once, not on project/theme/page changes');
|
|
19
|
+
for (const contract of ['kb:theme', 'kb:select-project', 'kb:scope', 'kb:knowledge', 'kb:ready']) assert.ok(script.includes(contract));
|
|
20
|
+
assert.ok(script.includes('event.origin !== location.origin') && script.includes('contentWindow === event.source'), 'module messages must be from the expected frame and origin');
|
|
21
|
+
assert.ok(script.includes('state.epoch++') && script.includes('referenceEpoch') && script.includes('logEpoch'), 'late project/reference/log feedback must be isolated');
|
|
22
|
+
assert.match(script, /on\('domainForm'[\s\S]*?await refreshRegistry\(true\)/, 'domain writes must invalidate pre-mutation reads');
|
|
23
|
+
assert.match(script, /registered = true;[\s\S]*?await refreshRegistry\(true\)/, 'import must not reuse a pre-import snapshot');
|
|
24
|
+
assert.ok(script.includes("title: String(r.title || r.entryId || '')") && script.includes('esc(m.text)') && script.includes('esc(project.name)'));
|
|
25
|
+
assert.ok(script.includes('text.textContent = record.message') && script.includes("'logs/appended'"), 'diagnostics stay inert and stream live');
|
|
26
|
+
assert.ok(script.includes('URL.revokeObjectURL(url)') && script.includes('logStream?.close()'));
|
|
27
|
+
assert.ok(script.includes('resumeImport = true') && script.includes("$('importDialog').showModal()") && script.includes("p.code === 'KNOWLEDGE_ROOT_MISSING'"), 'first import can return from setup without resetting its input');
|
|
28
|
+
assert.ok(css.includes('height:100%') && css.includes('.view.module-view{padding:0;overflow:hidden}') && css.includes('overflow-wrap:anywhere'));
|
|
29
|
+
assert.ok(css.includes('html[data-theme=dark]') && css.includes('prefers-reduced-motion'));
|
|
30
|
+
assert.doesNotThrow(() => new Function(script));
|
|
31
|
+
console.log('workspace UI contract test PASS (source contract; not browser acceptance)');
|