@sanqianx/project-knowledge 5.0.19 → 5.0.20
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/.agents/plugins/marketplace.json +3 -3
- package/.claude-plugin/marketplace.json +1 -1
- package/CHANGELOG.md +5 -0
- package/LICENSE +201 -201
- package/NOTICE +5 -5
- package/README.md +349 -349
- package/_modules/knowledge-engine/README.md +4 -0
- package/_modules/knowledge-engine/src/analysis-failures.js +39 -0
- package/_modules/knowledge-engine/src/analysis-worker.js +6 -2
- package/_modules/knowledge-engine/src/coordinator.js +7 -0
- package/_modules/knowledge-engine/src/headless-analyzer.js +2 -1
- package/_modules/knowledge-engine/src/server.js +1 -1
- package/_site/_test/agent-capture-mcp-ui-test.js +67 -67
- package/_site/_test/ai-profile-test.js +75 -75
- package/_site/_test/ai-vendor-presets-test.js +91 -91
- package/_site/_test/automatic-context-test.js +70 -70
- package/_site/_test/bin-cli-test.js +230 -230
- package/_site/_test/bridge-adapter-test.js +95 -95
- package/_site/_test/bridge-consumer-service-test.js +310 -310
- package/_site/_test/candidate-bin-content-test.js +35 -35
- package/_site/_test/candidate-product-assets-test.js +13 -13
- package/_site/_test/candidate-release-contract-test.js +14 -14
- package/_site/_test/capture-hook-dedup-test.js +158 -158
- package/_site/_test/claude-executable-discovery-test.js +28 -28
- package/_site/_test/claude-session-lifecycle-sweep-test.js +252 -252
- package/_site/_test/cli-instance-management-test.js +62 -62
- package/_site/_test/client-capture-restart-e2e-test.js +189 -189
- package/_site/_test/codex-conversation-projection-test.js +60 -60
- package/_site/_test/codex-marketplace-scoped-path-test.js +186 -186
- package/_site/_test/codex-notify-capture-e2e-test.js +101 -101
- package/_site/_test/codex-notify-fanout-test.js +134 -134
- package/_site/_test/codex-notify-upgrade-test.js +253 -253
- package/_site/_test/commit-boundary-freeze-test.js +147 -147
- package/_site/_test/commit-conversation-binding-test.js +4 -4
- package/_site/_test/configuration-generation-test.js +22 -22
- package/_site/_test/context-pack-test.js +200 -200
- package/_site/_test/conversation-api-test.js +10 -10
- package/_site/_test/conversation-origin-test.js +15 -15
- package/_site/_test/cross-repo-isolation-e2e-test.js +187 -187
- package/_site/_test/embedding-config-test.js +32 -32
- package/_site/_test/fixtures/fake-browser-exit.js +10 -10
- package/_site/_test/fixtures/fake-browser-hang.js +10 -10
- package/_site/_test/fixtures/integration-busy-worker.cjs +6 -6
- package/_site/_test/fixtures/make-git-repos.js +92 -92
- package/_site/_test/fixtures/retrieval-gold-queries.json +32 -32
- package/_site/_test/fresh-hook-reimport-test.js +53 -53
- package/_site/_test/fresh-product-data-test.js +56 -56
- package/_site/_test/git-path-compatibility-test.js +80 -80
- package/_site/_test/git-validation-test.js +81 -81
- package/_site/_test/hook-worktree-test.js +42 -42
- package/_site/_test/index-builder-test.js +41 -41
- package/_site/_test/index-error-display-contract-test.js +10 -10
- package/_site/_test/integration-adapters-test.js +67 -67
- package/_site/_test/integration-capture-setup-test.js +118 -118
- package/_site/_test/integration-runtime-binding-a2-test.js +566 -566
- package/_site/_test/integration-runtime-binding-a3-test.js +227 -227
- package/_site/_test/integration-runtime-binding-test.js +473 -473
- package/_site/_test/integration-status-scan-test.js +29 -29
- package/_site/_test/kb-framework-test.js +57 -57
- package/_site/_test/kbpath-follow-test.js +63 -63
- package/_site/_test/knowledge-db-test.js +144 -144
- package/_site/_test/knowledge-engine-client-test.js +109 -109
- package/_site/_test/knowledge-maintenance-test.js +40 -40
- package/_site/_test/knowledge-panel-ui-contract-test.js +112 -0
- package/_site/_test/knowledge-scopes-test.js +37 -37
- package/_site/_test/knowledge-storage-location-test.js +28 -28
- package/_site/_test/launcher-readiness-test.js +80 -80
- package/_site/_test/legacy-claude-session-alias-test.js +106 -106
- package/_site/_test/legacy-routes-removed-test.js +47 -47
- package/_site/_test/legacy-workbench-exclusion-test.js +92 -92
- package/_site/_test/live-gateway-test.js +48 -48
- package/_site/_test/local-marketplace-test.js +16 -16
- package/_site/_test/log-redaction-test.js +58 -58
- package/_site/_test/markdown-maintenance-test.js +11 -11
- package/_site/_test/model-context-windows-test.js +89 -89
- package/_site/_test/module-agent-start-proxy-test.js +65 -65
- package/_site/_test/module-artifact-integrity-test.js +27 -27
- package/_site/_test/module-boundary-test.js +30 -30
- package/_site/_test/module-bridge-eventbridge-test.js +202 -202
- package/_site/_test/module-dynamic-port-test.js +118 -118
- package/_site/_test/module-index-proxy-test.js +33 -33
- package/_site/_test/module-model-configuration-test.js +44 -44
- package/_site/_test/module-process-lifecycle-test.js +35 -35
- package/_site/_test/module-stream-proxy-test.js +34 -34
- package/_site/_test/module-terminal-restart-test.js +63 -63
- package/_site/_test/module-wiring-unbind-race-test.js +59 -59
- package/_site/_test/native-context-integrations-test.js +86 -86
- package/_site/_test/orphaned-claim-recovery-test.js +80 -80
- package/_site/_test/package-boundary-test.js +43 -42
- package/_site/_test/package-startup-test.js +56 -56
- package/_site/_test/packaged-ui-smoke-test.js +88 -88
- package/_site/_test/pr-consumer-contract-test.js +55 -55
- package/_site/_test/product-brand-contract-test.js +25 -25
- package/_site/_test/product-diagnostics-ui-test.js +93 -93
- package/_site/_test/product-import-ui-test.js +104 -104
- package/_site/_test/product-progress-status-test.js +12 -1
- package/_site/_test/project-remove-running-guard-test.js +79 -79
- package/_site/_test/project-store-test.js +90 -90
- package/_site/_test/registry-events-test.js +38 -38
- package/_site/_test/release-version-sync-test.js +115 -115
- package/_site/_test/requirement-binding-test.js +113 -113
- package/_site/_test/runtime-endpoint-test.js +64 -64
- package/_site/_test/scanner-test.js +83 -83
- package/_site/_test/server-security-test.js +96 -96
- package/_site/_test/sessions-stream-test.js +93 -93
- package/_site/_test/shared-scope-root-binding-test.js +67 -67
- package/_site/_test/simple-import-test.js +98 -98
- package/_site/_test/startup-analysis-disabled-test.js +6 -6
- package/_site/_test/team-knowledge-store-sparse-checkout-test.js +196 -196
- package/_site/_test/team-knowledge-store-test.js +132 -132
- package/_site/_test/test-server-cleanup-test.js +35 -35
- package/_site/_test/test-server-module-isolation-test.js +37 -37
- package/_site/_test/tracking-start-test.js +35 -35
- package/_site/_test/workbench-context-receipts-test.js +22 -22
- package/_site/_test/workspace-ui-contract-test.js +31 -31
- package/_site/_test/zcode-capture-integration-test.js +108 -108
- package/_site/lib/ai-vendor-presets.js +83 -83
- package/_site/lib/ai-workspace.js +3 -3
- package/_site/lib/atomic-file.js +204 -204
- package/_site/lib/bridge-adapter.js +260 -260
- package/_site/lib/capture-hook-config.js +108 -108
- package/_site/lib/claude-cli-runner.js +89 -89
- package/_site/lib/cli-instances.js +43 -43
- package/_site/lib/codex-context-status.js +17 -17
- package/_site/lib/codex-notify-config.js +193 -193
- package/_site/lib/commit-conversation-binder.js +12 -12
- package/_site/lib/commit-reconciler.js +38 -38
- package/_site/lib/context-pack-builder.js +299 -299
- package/_site/lib/conversation-exclusions.js +204 -204
- package/_site/lib/conversation-query-service.js +17 -17
- package/_site/lib/data-dir.js +13 -13
- package/_site/lib/embedding-service.js +85 -85
- package/_site/lib/engine-connection.js +93 -93
- package/_site/lib/engine-knowledge-client.js +72 -72
- package/_site/lib/folder-picker-output.js +3 -3
- package/_site/lib/git-path.js +14 -14
- package/_site/lib/github-team-store.js +3 -3
- package/_site/lib/index-builder.js +157 -157
- package/_site/lib/integration-status-scan.js +99 -99
- package/_site/lib/integration-status-worker.js +14 -14
- package/_site/lib/kb-framework.js +15 -15
- package/_site/lib/knowledge-db.js +363 -363
- package/_site/lib/knowledge-schema.js +121 -121
- package/_site/lib/live-gateway.js +78 -78
- package/_site/lib/llm-client.js +12 -12
- package/_site/lib/model-context-windows.js +82 -82
- package/_site/lib/module-bridge.js +874 -874
- package/_site/lib/module-port-resolver.js +57 -57
- package/_site/lib/post-commit-automation.js +54 -54
- package/_site/lib/product-data.js +35 -35
- package/_site/lib/project-lifecycle-service.js +14 -14
- package/_site/lib/project-registry-store.js +129 -129
- package/_site/lib/prompt-registry.js +148 -148
- package/_site/lib/registry-events.js +16 -16
- package/_site/lib/requirement-adapters.js +10 -10
- package/_site/lib/requirement-binder.js +112 -112
- package/_site/lib/runtime-control.js +89 -89
- package/_site/lib/runtime-endpoint.js +5 -5
- package/_site/lib/zcode-context-integration.js +60 -60
- package/_site/scripts/codex-notify-fanout.cjs +95 -95
- package/_site/scripts/install-module-candidates.js +69 -69
- package/_site/scripts/pack-module-candidates.js +89 -89
- package/_site/scripts/sync-product-content.js +28 -28
- package/_site/scripts/sync-release-version.js +164 -164
- package/_site/scripts/vendor-modules.js +49 -49
- package/_site/scripts/verify-module-runtime.js +50 -50
- package/_site/scripts/verify-product-runtime.js +36 -36
- package/_site/scripts/verify-retrieval-gold.js +83 -83
- package/_site/server.js +15 -15
- package/_site/start.bat +26 -26
- package/_site/stop.bat +11 -11
- package/bin/project-knowledge-integrations.js +254 -254
- package/bin/project-knowledge-kb.js +67 -67
- package/bin/project-knowledge-mcp.js +8 -8
- package/bin/project-knowledge.js +170 -170
- package/claude-prompts.json +21 -21
- package/docs/README.zh-CN.md +205 -205
- package/docs/assets/architecture.svg +181 -181
- package/docs/assets/logo.svg +49 -49
- package/docs/fresh-v5-start.md +42 -42
- package/docs/leader-glm-workbench-playbook.md +122 -122
- package/docs/project-registry-schema.md +11 -11
- package/docs/release-5.0.16-acceptance.md +176 -176
- package/docs/release-5.0.17-acceptance.md +117 -117
- package/docs/release-5.0.19-acceptance.md +39 -25
- package/docs/team-knowledge-mode-a-plan.md +73 -73
- package/module-runtime-manifest.json +53 -33
- package/package.json +3 -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/plugins/project-knowledge/opencode/project-knowledge.md +3 -3
- package/plugins/project-knowledge/skills/project-knowledge/SKILL.md +28 -28
- package/templates/change.md +30 -30
- package/templates/project-readme.md +18 -18
- package/ui/index.html +1 -1
- package/ui/knowledge-panel.html +118 -0
- package/ui/product.css +8 -0
- package/ui/product.js +146 -4
- package/vendor-manifest.json +55 -54
|
@@ -1,118 +1,118 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
const assert = require('node:assert/strict');
|
|
3
|
-
const http = require('node:http');
|
|
4
|
-
const { resolveModuleUrl } = require('../lib/module-port-resolver');
|
|
5
|
-
const { ModuleBridge } = require('../lib/module-bridge');
|
|
6
|
-
|
|
7
|
-
const countingProbe = outcome => {
|
|
8
|
-
const seen = [];
|
|
9
|
-
return {
|
|
10
|
-
probe: async url => { seen.push(url); return outcome; },
|
|
11
|
-
get seen() { return seen; },
|
|
12
|
-
get calls() { return seen.length; },
|
|
13
|
-
};
|
|
14
|
-
};
|
|
15
|
-
|
|
16
|
-
const countingPortFree = isFree => {
|
|
17
|
-
const seen = [];
|
|
18
|
-
return {
|
|
19
|
-
portFree: async port => { seen.push(port); return isFree(port); },
|
|
20
|
-
get seen() { return seen; },
|
|
21
|
-
get calls() { return seen.length; },
|
|
22
|
-
};
|
|
23
|
-
};
|
|
24
|
-
|
|
25
|
-
(async () => {
|
|
26
|
-
// A: healthy module on the preferred port is reused without any port scan.
|
|
27
|
-
{
|
|
28
|
-
const probe = countingProbe('module');
|
|
29
|
-
const portFree = countingPortFree(() => false);
|
|
30
|
-
const resolved = await resolveModuleUrl({ kind: 'terminal', preferredUrl: 'http://127.0.0.1:5760', explicit: false, portFree: portFree.portFree, probe: probe.probe });
|
|
31
|
-
assert.equal(resolved.source, 'reused');
|
|
32
|
-
assert.equal(resolved.url, 'http://127.0.0.1:5760');
|
|
33
|
-
assert.equal(resolved.port, 5760);
|
|
34
|
-
assert.deepEqual(probe.seen, ['http://127.0.0.1:5760']);
|
|
35
|
-
assert.equal(portFree.calls, 0, 'reuse must never invoke portFree');
|
|
36
|
-
}
|
|
37
|
-
// B: foreign occupant on the preferred port moves the module to the next free port.
|
|
38
|
-
{
|
|
39
|
-
const probe = countingProbe('foreign');
|
|
40
|
-
const portFree = countingPortFree(port => port === 5761);
|
|
41
|
-
const resolved = await resolveModuleUrl({ kind: 'terminal', preferredUrl: 'http://127.0.0.1:5760', explicit: false, portFree: portFree.portFree, probe: probe.probe });
|
|
42
|
-
assert.equal(resolved.source, 'dynamic');
|
|
43
|
-
assert.equal(resolved.url, 'http://127.0.0.1:5761');
|
|
44
|
-
assert.equal(resolved.port, 5761);
|
|
45
|
-
assert.equal(resolved.offset, 1);
|
|
46
|
-
assert.deepEqual(portFree.seen, [5760, 5761]);
|
|
47
|
-
}
|
|
48
|
-
// C: unreachable preferred port skips three busy ports before picking the fourth.
|
|
49
|
-
{
|
|
50
|
-
const probe = countingProbe('unreachable');
|
|
51
|
-
const portFree = countingPortFree(port => port === 8790);
|
|
52
|
-
const resolved = await resolveModuleUrl({ kind: 'vector-hub', preferredUrl: 'http://127.0.0.1:8787', explicit: false, portFree: portFree.portFree, probe: probe.probe });
|
|
53
|
-
assert.equal(resolved.source, 'dynamic');
|
|
54
|
-
assert.equal(resolved.url, 'http://127.0.0.1:8790');
|
|
55
|
-
assert.equal(resolved.port, 8790);
|
|
56
|
-
assert.equal(resolved.offset, 3);
|
|
57
|
-
assert.deepEqual(portFree.seen, [8787, 8788, 8789, 8790]);
|
|
58
|
-
}
|
|
59
|
-
// D: exhausted scan range fails loudly with kind and port span in the message.
|
|
60
|
-
{
|
|
61
|
-
const probe = countingProbe('unreachable');
|
|
62
|
-
const portFree = countingPortFree(() => false);
|
|
63
|
-
await assert.rejects(
|
|
64
|
-
() => resolveModuleUrl({ kind: 'event-bridge', preferredUrl: 'http://127.0.0.1:8790', explicit: false, portFree: portFree.portFree, probe: probe.probe }),
|
|
65
|
-
error => {
|
|
66
|
-
assert.equal(error.code, 'NO_FREE_MODULE_PORT');
|
|
67
|
-
assert.ok(error.message.includes('event-bridge'), `message must name the module kind: ${error.message}`);
|
|
68
|
-
assert.ok(error.message.includes('8790'), `message must name the first scanned port: ${error.message}`);
|
|
69
|
-
assert.ok(error.message.includes('8809'), `message must name the last scanned port: ${error.message}`);
|
|
70
|
-
return true;
|
|
71
|
-
},
|
|
72
|
-
);
|
|
73
|
-
assert.equal(portFree.calls, 20, 'default scanRange must probe 20 consecutive ports before failing');
|
|
74
|
-
assert.deepEqual(portFree.seen, Array.from({ length: 20 }, (_, index) => 8790 + index));
|
|
75
|
-
}
|
|
76
|
-
// E: an explicit external URL short-circuits before any probe or port scan.
|
|
77
|
-
{
|
|
78
|
-
const probe = countingProbe('module');
|
|
79
|
-
const portFree = countingPortFree(() => true);
|
|
80
|
-
const resolved = await resolveModuleUrl({ kind: 'knowledge-engine', preferredUrl: 'http://192.168.1.5:5790', explicit: true, portFree: portFree.portFree, probe: probe.probe });
|
|
81
|
-
assert.equal(resolved.source, 'external');
|
|
82
|
-
assert.equal(resolved.url, 'http://192.168.1.5:5790');
|
|
83
|
-
assert.equal(probe.calls, 0, 'explicit external URL must never be probed');
|
|
84
|
-
assert.equal(portFree.calls, 0, 'explicit external URL must never trigger a port scan');
|
|
85
|
-
}
|
|
86
|
-
// F: a preferred URL without an explicit port is rejected.
|
|
87
|
-
{
|
|
88
|
-
const probe = countingProbe('module');
|
|
89
|
-
const portFree = countingPortFree(() => true);
|
|
90
|
-
await assert.rejects(
|
|
91
|
-
() => resolveModuleUrl({ kind: 'terminal', preferredUrl: 'http://127.0.0.1', explicit: false, portFree: portFree.portFree, probe: probe.probe }),
|
|
92
|
-
error => { assert.equal(error.code, 'INVALID_MODULE_URL'); return true; },
|
|
93
|
-
);
|
|
94
|
-
}
|
|
95
|
-
// G (integration): an HTTP service squatting on the preferred port with
|
|
96
|
-
// 404 responses — exactly what happens when the shell's own fallback range
|
|
97
|
-
// 5757..5776 lands on the terminal default 5760 — must not be misread as a
|
|
98
|
-
// module and must not crash-loop: the bridge moves to the next free port.
|
|
99
|
-
{
|
|
100
|
-
const squatter = http.createServer((req, res) => { res.writeHead(404, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ error: 'not found' })); });
|
|
101
|
-
await new Promise(resolve => squatter.listen(0, '127.0.0.1', resolve));
|
|
102
|
-
const squatterPort = squatter.address().port;
|
|
103
|
-
const bridge = new ModuleBridge();
|
|
104
|
-
// Isolate the terminal leg: everything else is explicit/external so the
|
|
105
|
-
// resolution loop only probes the squatter port for the terminal kind.
|
|
106
|
-
bridge.terminalUrl = `http://127.0.0.1:${squatterPort}`;
|
|
107
|
-
bridge.explicitModuleUrls = { terminal: false, vectorHub: true, eventBridge: true, engine: true };
|
|
108
|
-
try {
|
|
109
|
-
await bridge._resolveManagedUrls();
|
|
110
|
-
assert.equal(bridge.moduleUrlSources.terminal, 'dynamic', '404 squatter must force a dynamic move, never reuse');
|
|
111
|
-
assert.equal(bridge.terminalUrl, `http://127.0.0.1:${squatterPort + 1}`, 'module moves to the port right after the squatter');
|
|
112
|
-
} finally {
|
|
113
|
-
squatter.closeAllConnections?.();
|
|
114
|
-
await new Promise(resolve => squatter.close(resolve));
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
console.log('module dynamic port test PASS');
|
|
118
|
-
})().catch(error => { console.error(error); process.exitCode = 1; });
|
|
1
|
+
'use strict';
|
|
2
|
+
const assert = require('node:assert/strict');
|
|
3
|
+
const http = require('node:http');
|
|
4
|
+
const { resolveModuleUrl } = require('../lib/module-port-resolver');
|
|
5
|
+
const { ModuleBridge } = require('../lib/module-bridge');
|
|
6
|
+
|
|
7
|
+
const countingProbe = outcome => {
|
|
8
|
+
const seen = [];
|
|
9
|
+
return {
|
|
10
|
+
probe: async url => { seen.push(url); return outcome; },
|
|
11
|
+
get seen() { return seen; },
|
|
12
|
+
get calls() { return seen.length; },
|
|
13
|
+
};
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
const countingPortFree = isFree => {
|
|
17
|
+
const seen = [];
|
|
18
|
+
return {
|
|
19
|
+
portFree: async port => { seen.push(port); return isFree(port); },
|
|
20
|
+
get seen() { return seen; },
|
|
21
|
+
get calls() { return seen.length; },
|
|
22
|
+
};
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
(async () => {
|
|
26
|
+
// A: healthy module on the preferred port is reused without any port scan.
|
|
27
|
+
{
|
|
28
|
+
const probe = countingProbe('module');
|
|
29
|
+
const portFree = countingPortFree(() => false);
|
|
30
|
+
const resolved = await resolveModuleUrl({ kind: 'terminal', preferredUrl: 'http://127.0.0.1:5760', explicit: false, portFree: portFree.portFree, probe: probe.probe });
|
|
31
|
+
assert.equal(resolved.source, 'reused');
|
|
32
|
+
assert.equal(resolved.url, 'http://127.0.0.1:5760');
|
|
33
|
+
assert.equal(resolved.port, 5760);
|
|
34
|
+
assert.deepEqual(probe.seen, ['http://127.0.0.1:5760']);
|
|
35
|
+
assert.equal(portFree.calls, 0, 'reuse must never invoke portFree');
|
|
36
|
+
}
|
|
37
|
+
// B: foreign occupant on the preferred port moves the module to the next free port.
|
|
38
|
+
{
|
|
39
|
+
const probe = countingProbe('foreign');
|
|
40
|
+
const portFree = countingPortFree(port => port === 5761);
|
|
41
|
+
const resolved = await resolveModuleUrl({ kind: 'terminal', preferredUrl: 'http://127.0.0.1:5760', explicit: false, portFree: portFree.portFree, probe: probe.probe });
|
|
42
|
+
assert.equal(resolved.source, 'dynamic');
|
|
43
|
+
assert.equal(resolved.url, 'http://127.0.0.1:5761');
|
|
44
|
+
assert.equal(resolved.port, 5761);
|
|
45
|
+
assert.equal(resolved.offset, 1);
|
|
46
|
+
assert.deepEqual(portFree.seen, [5760, 5761]);
|
|
47
|
+
}
|
|
48
|
+
// C: unreachable preferred port skips three busy ports before picking the fourth.
|
|
49
|
+
{
|
|
50
|
+
const probe = countingProbe('unreachable');
|
|
51
|
+
const portFree = countingPortFree(port => port === 8790);
|
|
52
|
+
const resolved = await resolveModuleUrl({ kind: 'vector-hub', preferredUrl: 'http://127.0.0.1:8787', explicit: false, portFree: portFree.portFree, probe: probe.probe });
|
|
53
|
+
assert.equal(resolved.source, 'dynamic');
|
|
54
|
+
assert.equal(resolved.url, 'http://127.0.0.1:8790');
|
|
55
|
+
assert.equal(resolved.port, 8790);
|
|
56
|
+
assert.equal(resolved.offset, 3);
|
|
57
|
+
assert.deepEqual(portFree.seen, [8787, 8788, 8789, 8790]);
|
|
58
|
+
}
|
|
59
|
+
// D: exhausted scan range fails loudly with kind and port span in the message.
|
|
60
|
+
{
|
|
61
|
+
const probe = countingProbe('unreachable');
|
|
62
|
+
const portFree = countingPortFree(() => false);
|
|
63
|
+
await assert.rejects(
|
|
64
|
+
() => resolveModuleUrl({ kind: 'event-bridge', preferredUrl: 'http://127.0.0.1:8790', explicit: false, portFree: portFree.portFree, probe: probe.probe }),
|
|
65
|
+
error => {
|
|
66
|
+
assert.equal(error.code, 'NO_FREE_MODULE_PORT');
|
|
67
|
+
assert.ok(error.message.includes('event-bridge'), `message must name the module kind: ${error.message}`);
|
|
68
|
+
assert.ok(error.message.includes('8790'), `message must name the first scanned port: ${error.message}`);
|
|
69
|
+
assert.ok(error.message.includes('8809'), `message must name the last scanned port: ${error.message}`);
|
|
70
|
+
return true;
|
|
71
|
+
},
|
|
72
|
+
);
|
|
73
|
+
assert.equal(portFree.calls, 20, 'default scanRange must probe 20 consecutive ports before failing');
|
|
74
|
+
assert.deepEqual(portFree.seen, Array.from({ length: 20 }, (_, index) => 8790 + index));
|
|
75
|
+
}
|
|
76
|
+
// E: an explicit external URL short-circuits before any probe or port scan.
|
|
77
|
+
{
|
|
78
|
+
const probe = countingProbe('module');
|
|
79
|
+
const portFree = countingPortFree(() => true);
|
|
80
|
+
const resolved = await resolveModuleUrl({ kind: 'knowledge-engine', preferredUrl: 'http://192.168.1.5:5790', explicit: true, portFree: portFree.portFree, probe: probe.probe });
|
|
81
|
+
assert.equal(resolved.source, 'external');
|
|
82
|
+
assert.equal(resolved.url, 'http://192.168.1.5:5790');
|
|
83
|
+
assert.equal(probe.calls, 0, 'explicit external URL must never be probed');
|
|
84
|
+
assert.equal(portFree.calls, 0, 'explicit external URL must never trigger a port scan');
|
|
85
|
+
}
|
|
86
|
+
// F: a preferred URL without an explicit port is rejected.
|
|
87
|
+
{
|
|
88
|
+
const probe = countingProbe('module');
|
|
89
|
+
const portFree = countingPortFree(() => true);
|
|
90
|
+
await assert.rejects(
|
|
91
|
+
() => resolveModuleUrl({ kind: 'terminal', preferredUrl: 'http://127.0.0.1', explicit: false, portFree: portFree.portFree, probe: probe.probe }),
|
|
92
|
+
error => { assert.equal(error.code, 'INVALID_MODULE_URL'); return true; },
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
// G (integration): an HTTP service squatting on the preferred port with
|
|
96
|
+
// 404 responses — exactly what happens when the shell's own fallback range
|
|
97
|
+
// 5757..5776 lands on the terminal default 5760 — must not be misread as a
|
|
98
|
+
// module and must not crash-loop: the bridge moves to the next free port.
|
|
99
|
+
{
|
|
100
|
+
const squatter = http.createServer((req, res) => { res.writeHead(404, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ error: 'not found' })); });
|
|
101
|
+
await new Promise(resolve => squatter.listen(0, '127.0.0.1', resolve));
|
|
102
|
+
const squatterPort = squatter.address().port;
|
|
103
|
+
const bridge = new ModuleBridge();
|
|
104
|
+
// Isolate the terminal leg: everything else is explicit/external so the
|
|
105
|
+
// resolution loop only probes the squatter port for the terminal kind.
|
|
106
|
+
bridge.terminalUrl = `http://127.0.0.1:${squatterPort}`;
|
|
107
|
+
bridge.explicitModuleUrls = { terminal: false, vectorHub: true, eventBridge: true, engine: true };
|
|
108
|
+
try {
|
|
109
|
+
await bridge._resolveManagedUrls();
|
|
110
|
+
assert.equal(bridge.moduleUrlSources.terminal, 'dynamic', '404 squatter must force a dynamic move, never reuse');
|
|
111
|
+
assert.equal(bridge.terminalUrl, `http://127.0.0.1:${squatterPort + 1}`, 'module moves to the port right after the squatter');
|
|
112
|
+
} finally {
|
|
113
|
+
squatter.closeAllConnections?.();
|
|
114
|
+
await new Promise(resolve => squatter.close(resolve));
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
console.log('module dynamic port test PASS');
|
|
118
|
+
})().catch(error => { console.error(error); process.exitCode = 1; });
|
|
@@ -1,33 +1,33 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
const assert = require('node:assert/strict');
|
|
3
|
-
const http = require('node:http');
|
|
4
|
-
const { ModuleBridge } = require('../lib/module-bridge');
|
|
5
|
-
(async () => {
|
|
6
|
-
const upstream = http.createServer((req, res) => {
|
|
7
|
-
const timer = setTimeout(() => { res.writeHead(200, { 'Content-Type': 'application/json' }); res.end('{"ok":true}'); }, 75);
|
|
8
|
-
res.on('close', () => clearTimeout(timer));
|
|
9
|
-
});
|
|
10
|
-
await new Promise(resolve => upstream.listen(0, '127.0.0.1', resolve));
|
|
11
|
-
const bridge = new ModuleBridge({ vectorHubUrl: `http://127.0.0.1:${upstream.address().port}` });
|
|
12
|
-
const timer = global.setTimeout;
|
|
13
|
-
const budgets = [];
|
|
14
|
-
// Scale only the two product deadlines, preserving real HTTP cancellation.
|
|
15
|
-
global.setTimeout = (fn, ms, ...args) => {
|
|
16
|
-
if (ms === 10000 || ms === 120000) { budgets.push(ms); return timer(fn, ms === 10000 ? 25 : 500, ...args); }
|
|
17
|
-
return timer(fn, ms, ...args);
|
|
18
|
-
};
|
|
19
|
-
const server = http.createServer((req, res) => bridge.proxy(req, res, `http://127.0.0.1:${upstream.address().port}${req.url}`));
|
|
20
|
-
await new Promise(resolve => server.listen(0, '127.0.0.1', resolve));
|
|
21
|
-
try {
|
|
22
|
-
const base = `http://127.0.0.1:${server.address().port}`;
|
|
23
|
-
const indexed = await fetch(`${base}/api/sync`, { method: 'POST', body: '{}' });
|
|
24
|
-
assert.equal(indexed.status, 200, 'a real embedding pass must use the engine indexing budget rather than the interactive deadline');
|
|
25
|
-
assert.equal(budgets[0], 120000);
|
|
26
|
-
const health = await fetch(`${base}/api/health`);
|
|
27
|
-
assert.equal(health.status, 502, 'ordinary reads keep their bounded interactive deadline');
|
|
28
|
-
console.log('module-index-proxy-test PASS');
|
|
29
|
-
} finally {
|
|
30
|
-
global.setTimeout = timer;
|
|
31
|
-
for (const s of [server, upstream]) { s.closeAllConnections(); await new Promise(resolve => s.close(resolve)); }
|
|
32
|
-
}
|
|
33
|
-
})().catch(error => { console.error(error); process.exitCode = 1; });
|
|
1
|
+
'use strict';
|
|
2
|
+
const assert = require('node:assert/strict');
|
|
3
|
+
const http = require('node:http');
|
|
4
|
+
const { ModuleBridge } = require('../lib/module-bridge');
|
|
5
|
+
(async () => {
|
|
6
|
+
const upstream = http.createServer((req, res) => {
|
|
7
|
+
const timer = setTimeout(() => { res.writeHead(200, { 'Content-Type': 'application/json' }); res.end('{"ok":true}'); }, 75);
|
|
8
|
+
res.on('close', () => clearTimeout(timer));
|
|
9
|
+
});
|
|
10
|
+
await new Promise(resolve => upstream.listen(0, '127.0.0.1', resolve));
|
|
11
|
+
const bridge = new ModuleBridge({ vectorHubUrl: `http://127.0.0.1:${upstream.address().port}` });
|
|
12
|
+
const timer = global.setTimeout;
|
|
13
|
+
const budgets = [];
|
|
14
|
+
// Scale only the two product deadlines, preserving real HTTP cancellation.
|
|
15
|
+
global.setTimeout = (fn, ms, ...args) => {
|
|
16
|
+
if (ms === 10000 || ms === 120000) { budgets.push(ms); return timer(fn, ms === 10000 ? 25 : 500, ...args); }
|
|
17
|
+
return timer(fn, ms, ...args);
|
|
18
|
+
};
|
|
19
|
+
const server = http.createServer((req, res) => bridge.proxy(req, res, `http://127.0.0.1:${upstream.address().port}${req.url}`));
|
|
20
|
+
await new Promise(resolve => server.listen(0, '127.0.0.1', resolve));
|
|
21
|
+
try {
|
|
22
|
+
const base = `http://127.0.0.1:${server.address().port}`;
|
|
23
|
+
const indexed = await fetch(`${base}/api/sync`, { method: 'POST', body: '{}' });
|
|
24
|
+
assert.equal(indexed.status, 200, 'a real embedding pass must use the engine indexing budget rather than the interactive deadline');
|
|
25
|
+
assert.equal(budgets[0], 120000);
|
|
26
|
+
const health = await fetch(`${base}/api/health`);
|
|
27
|
+
assert.equal(health.status, 502, 'ordinary reads keep their bounded interactive deadline');
|
|
28
|
+
console.log('module-index-proxy-test PASS');
|
|
29
|
+
} finally {
|
|
30
|
+
global.setTimeout = timer;
|
|
31
|
+
for (const s of [server, upstream]) { s.closeAllConnections(); await new Promise(resolve => s.close(resolve)); }
|
|
32
|
+
}
|
|
33
|
+
})().catch(error => { console.error(error); process.exitCode = 1; });
|
|
@@ -1,44 +1,44 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
const assert = require('node:assert/strict');
|
|
3
|
-
const http = require('node:http');
|
|
4
|
-
const { ModuleBridge } = require('../lib/module-bridge');
|
|
5
|
-
(async () => {
|
|
6
|
-
let profile = { apiKey: 'controlled-model-key', mainModel: 'durable-default' };
|
|
7
|
-
let settings = { provider: 'openai', apiKey: 'controlled-vector-key', model: 'embedding-model' };
|
|
8
|
-
let appliedModel, appliedVector; const hits = [];
|
|
9
|
-
const migrationId = `embedding-${'a'.repeat(64)}`;
|
|
10
|
-
let legacy = { provider: 'minimax', apiKey: 'controlled-legacy-key', model: 'embo-01' };
|
|
11
|
-
const service = http.createServer((req, res) => {
|
|
12
|
-
assert.equal(req.headers.authorization, 'Bearer runtime-test');
|
|
13
|
-
assert.equal(req.headers.origin, undefined);
|
|
14
|
-
let input = ''; req.on('data', c => { input += c; });
|
|
15
|
-
req.on('end', () => {
|
|
16
|
-
hits.push(req.url); const body = JSON.parse(input || '{}');
|
|
17
|
-
let result = { ok: true }; let status = 200;
|
|
18
|
-
if (req.url.endsWith('/internal/maintenance-config')) { result = profile; if (!profile) status = 409; }
|
|
19
|
-
else if (req.url === '/api/internal/embedding-config') result = { settings: legacy, migrationId };
|
|
20
|
-
else if (req.url.endsWith('/internal/import-embedding')) { assert.equal(body.settings.apiKey, 'controlled-legacy-key'); }
|
|
21
|
-
else if (req.url === '/api/internal/retire-embedding') { assert.equal(body.migrationId, migrationId); legacy = null; }
|
|
22
|
-
else if (req.url.endsWith('/v1/internal/embedding-config')) { result = settings; if (!settings) status = 409; }
|
|
23
|
-
else if (req.url === '/api/internal/runtime-config') appliedVector = body.settings;
|
|
24
|
-
else if (req.url === '/api/runtime-config') appliedModel = body.profile;
|
|
25
|
-
res.writeHead(status, { 'Content-Type': 'application/json' }); res.end(JSON.stringify(result || {}));
|
|
26
|
-
});
|
|
27
|
-
});
|
|
28
|
-
await new Promise(resolve => service.listen(0, '127.0.0.1', resolve));
|
|
29
|
-
const url = `http://127.0.0.1:${service.address().port}`;
|
|
30
|
-
const bridge = new ModuleBridge({ terminalUrl: url, engineUrl: url, vectorHubUrl: url, runtimeToken: 'runtime-test' });
|
|
31
|
-
try {
|
|
32
|
-
const status = await bridge.syncEngineConfiguration();
|
|
33
|
-
assert.equal(status.model, true); assert.equal(status.embedding, true);
|
|
34
|
-
assert.equal(appliedModel.mainModel, 'durable-default'); assert.equal(appliedVector.model, 'embedding-model');
|
|
35
|
-
assert.ok(!JSON.stringify(status).includes('key'));
|
|
36
|
-
assert.ok(!hits.some(route => /import-embedding|retire-embedding/.test(route)), 'fresh product must not import old credentials');
|
|
37
|
-
assert.ok(!hits.includes('/api/internal/embedding-config'));
|
|
38
|
-
profile = null; settings = null;
|
|
39
|
-
const revoked = await bridge.syncEngineConfiguration();
|
|
40
|
-
assert.equal(revoked.model, false); assert.equal(revoked.embedding, false);
|
|
41
|
-
assert.equal(appliedModel, null); assert.equal(appliedVector, null);
|
|
42
|
-
console.log('module-model-configuration-test PASS');
|
|
43
|
-
} finally { service.closeAllConnections(); await new Promise(resolve => service.close(resolve)); }
|
|
44
|
-
})().catch(error => { console.error(error); process.exitCode = 1; });
|
|
1
|
+
'use strict';
|
|
2
|
+
const assert = require('node:assert/strict');
|
|
3
|
+
const http = require('node:http');
|
|
4
|
+
const { ModuleBridge } = require('../lib/module-bridge');
|
|
5
|
+
(async () => {
|
|
6
|
+
let profile = { apiKey: 'controlled-model-key', mainModel: 'durable-default' };
|
|
7
|
+
let settings = { provider: 'openai', apiKey: 'controlled-vector-key', model: 'embedding-model' };
|
|
8
|
+
let appliedModel, appliedVector; const hits = [];
|
|
9
|
+
const migrationId = `embedding-${'a'.repeat(64)}`;
|
|
10
|
+
let legacy = { provider: 'minimax', apiKey: 'controlled-legacy-key', model: 'embo-01' };
|
|
11
|
+
const service = http.createServer((req, res) => {
|
|
12
|
+
assert.equal(req.headers.authorization, 'Bearer runtime-test');
|
|
13
|
+
assert.equal(req.headers.origin, undefined);
|
|
14
|
+
let input = ''; req.on('data', c => { input += c; });
|
|
15
|
+
req.on('end', () => {
|
|
16
|
+
hits.push(req.url); const body = JSON.parse(input || '{}');
|
|
17
|
+
let result = { ok: true }; let status = 200;
|
|
18
|
+
if (req.url.endsWith('/internal/maintenance-config')) { result = profile; if (!profile) status = 409; }
|
|
19
|
+
else if (req.url === '/api/internal/embedding-config') result = { settings: legacy, migrationId };
|
|
20
|
+
else if (req.url.endsWith('/internal/import-embedding')) { assert.equal(body.settings.apiKey, 'controlled-legacy-key'); }
|
|
21
|
+
else if (req.url === '/api/internal/retire-embedding') { assert.equal(body.migrationId, migrationId); legacy = null; }
|
|
22
|
+
else if (req.url.endsWith('/v1/internal/embedding-config')) { result = settings; if (!settings) status = 409; }
|
|
23
|
+
else if (req.url === '/api/internal/runtime-config') appliedVector = body.settings;
|
|
24
|
+
else if (req.url === '/api/runtime-config') appliedModel = body.profile;
|
|
25
|
+
res.writeHead(status, { 'Content-Type': 'application/json' }); res.end(JSON.stringify(result || {}));
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
await new Promise(resolve => service.listen(0, '127.0.0.1', resolve));
|
|
29
|
+
const url = `http://127.0.0.1:${service.address().port}`;
|
|
30
|
+
const bridge = new ModuleBridge({ terminalUrl: url, engineUrl: url, vectorHubUrl: url, runtimeToken: 'runtime-test' });
|
|
31
|
+
try {
|
|
32
|
+
const status = await bridge.syncEngineConfiguration();
|
|
33
|
+
assert.equal(status.model, true); assert.equal(status.embedding, true);
|
|
34
|
+
assert.equal(appliedModel.mainModel, 'durable-default'); assert.equal(appliedVector.model, 'embedding-model');
|
|
35
|
+
assert.ok(!JSON.stringify(status).includes('key'));
|
|
36
|
+
assert.ok(!hits.some(route => /import-embedding|retire-embedding/.test(route)), 'fresh product must not import old credentials');
|
|
37
|
+
assert.ok(!hits.includes('/api/internal/embedding-config'));
|
|
38
|
+
profile = null; settings = null;
|
|
39
|
+
const revoked = await bridge.syncEngineConfiguration();
|
|
40
|
+
assert.equal(revoked.model, false); assert.equal(revoked.embedding, false);
|
|
41
|
+
assert.equal(appliedModel, null); assert.equal(appliedVector, null);
|
|
42
|
+
console.log('module-model-configuration-test PASS');
|
|
43
|
+
} finally { service.closeAllConnections(); await new Promise(resolve => service.close(resolve)); }
|
|
44
|
+
})().catch(error => { console.error(error); process.exitCode = 1; });
|
|
@@ -1,35 +1,35 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
const fs = require('node:fs');
|
|
3
|
-
const os = require('node:os');
|
|
4
|
-
const path = require('node:path');
|
|
5
|
-
const assert = require('node:assert/strict');
|
|
6
|
-
const { ModuleBridge } = require('../lib/module-bridge');
|
|
7
|
-
(async () => {
|
|
8
|
-
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'kb-supervisor-lifecycle-'));
|
|
9
|
-
const fixture = path.join(root, '中文 module.js'), result = path.join(root, 'stopped.json');
|
|
10
|
-
fs.writeFileSync(fixture, "const fs=require('fs');process.on('message',m=>{if(m.type==='kb:shutdown'){fs.writeFileSync(process.argv[2],JSON.stringify({arg:process.argv[3],stopped:true}));process.exit(0);}});process.send({ready:true});");
|
|
11
|
-
const bridge = new ModuleBridge();
|
|
12
|
-
bridge._spawnSupervised({ name: 'fixture', command: `node ${JSON.stringify(fixture)} ${JSON.stringify(result)} ${JSON.stringify('中文 argument with spaces')} ` });
|
|
13
|
-
const child = bridge.supervised[0];
|
|
14
|
-
await new Promise((resolve, reject) => { child.once('message', resolve); child.once('error', reject); });
|
|
15
|
-
await bridge.stopSupervised();
|
|
16
|
-
assert.equal(child.exitCode, 0, 'IPC shutdown must finish before the shell releases its process');
|
|
17
|
-
assert.deepEqual(JSON.parse(fs.readFileSync(result)), { arg: '中文 argument with spaces', stopped: true });
|
|
18
|
-
const missing = new ModuleBridge();
|
|
19
|
-
missing._spawnSupervised({ name: 'missing', command: 'kb-definitely-missing-executable' });
|
|
20
|
-
await new Promise(resolve => setTimeout(resolve, 100));
|
|
21
|
-
assert.equal(missing.supervised.length, 0, 'failed process start must not crash or leave a false running module');
|
|
22
|
-
await missing.stopSupervised();
|
|
23
|
-
const { Logger, LogRepository } = require('../lib/structured-logger');
|
|
24
|
-
const { StorageLayout } = require('../lib/storage-layout');
|
|
25
|
-
const layout = new StorageLayout({ dataDir: path.join(root, 'data') }); const logger = new Logger({ layout });
|
|
26
|
-
const diagnosticFixture = path.join(root, 'diagnostic.js');
|
|
27
|
-
fs.writeFileSync(diagnosticFixture, "process.on('message',m=>{if(m.type==='kb:shutdown')process.exit(0)});process.send({type:'kb:diagnostic',record:{level:'error',event:'knowledge.maintenance.failed',message:'维护失败',projectId:'project-diagnostic',commitSha:'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa',phase:'validating',error:{code:'ANALYSIS_EMPTY_DOCUMENT',message:'知识文档正文为空'}}});");
|
|
28
|
-
const supervised = new ModuleBridge({ logger }); supervised._spawnSupervised({ name: 'knowledge-engine', command: `node ${JSON.stringify(diagnosticFixture)}` });
|
|
29
|
-
await new Promise((resolve, reject) => { supervised.supervised[0].once('message', resolve); supervised.supervised[0].once('error', reject); });
|
|
30
|
-
await logger.flush();
|
|
31
|
-
const records = new LogRepository({ layout }).query({ commitSha: 'a'.repeat(40) }).entries;
|
|
32
|
-
assert(records.some(record => record.event === 'knowledge.maintenance.failed' && record.error.message === '知识文档正文为空' && record.phase === 'validating'));
|
|
33
|
-
await supervised.stopSupervised(); await logger.close();
|
|
34
|
-
console.log('module process lifecycle test PASS');
|
|
35
|
-
})().catch(error => { console.error(error); process.exitCode = 1; });
|
|
1
|
+
'use strict';
|
|
2
|
+
const fs = require('node:fs');
|
|
3
|
+
const os = require('node:os');
|
|
4
|
+
const path = require('node:path');
|
|
5
|
+
const assert = require('node:assert/strict');
|
|
6
|
+
const { ModuleBridge } = require('../lib/module-bridge');
|
|
7
|
+
(async () => {
|
|
8
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'kb-supervisor-lifecycle-'));
|
|
9
|
+
const fixture = path.join(root, '中文 module.js'), result = path.join(root, 'stopped.json');
|
|
10
|
+
fs.writeFileSync(fixture, "const fs=require('fs');process.on('message',m=>{if(m.type==='kb:shutdown'){fs.writeFileSync(process.argv[2],JSON.stringify({arg:process.argv[3],stopped:true}));process.exit(0);}});process.send({ready:true});");
|
|
11
|
+
const bridge = new ModuleBridge();
|
|
12
|
+
bridge._spawnSupervised({ name: 'fixture', command: `node ${JSON.stringify(fixture)} ${JSON.stringify(result)} ${JSON.stringify('中文 argument with spaces')} ` });
|
|
13
|
+
const child = bridge.supervised[0];
|
|
14
|
+
await new Promise((resolve, reject) => { child.once('message', resolve); child.once('error', reject); });
|
|
15
|
+
await bridge.stopSupervised();
|
|
16
|
+
assert.equal(child.exitCode, 0, 'IPC shutdown must finish before the shell releases its process');
|
|
17
|
+
assert.deepEqual(JSON.parse(fs.readFileSync(result)), { arg: '中文 argument with spaces', stopped: true });
|
|
18
|
+
const missing = new ModuleBridge();
|
|
19
|
+
missing._spawnSupervised({ name: 'missing', command: 'kb-definitely-missing-executable' });
|
|
20
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
21
|
+
assert.equal(missing.supervised.length, 0, 'failed process start must not crash or leave a false running module');
|
|
22
|
+
await missing.stopSupervised();
|
|
23
|
+
const { Logger, LogRepository } = require('../lib/structured-logger');
|
|
24
|
+
const { StorageLayout } = require('../lib/storage-layout');
|
|
25
|
+
const layout = new StorageLayout({ dataDir: path.join(root, 'data') }); const logger = new Logger({ layout });
|
|
26
|
+
const diagnosticFixture = path.join(root, 'diagnostic.js');
|
|
27
|
+
fs.writeFileSync(diagnosticFixture, "process.on('message',m=>{if(m.type==='kb:shutdown')process.exit(0)});process.send({type:'kb:diagnostic',record:{level:'error',event:'knowledge.maintenance.failed',message:'维护失败',projectId:'project-diagnostic',commitSha:'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa',phase:'validating',error:{code:'ANALYSIS_EMPTY_DOCUMENT',message:'知识文档正文为空'}}});");
|
|
28
|
+
const supervised = new ModuleBridge({ logger }); supervised._spawnSupervised({ name: 'knowledge-engine', command: `node ${JSON.stringify(diagnosticFixture)}` });
|
|
29
|
+
await new Promise((resolve, reject) => { supervised.supervised[0].once('message', resolve); supervised.supervised[0].once('error', reject); });
|
|
30
|
+
await logger.flush();
|
|
31
|
+
const records = new LogRepository({ layout }).query({ commitSha: 'a'.repeat(40) }).entries;
|
|
32
|
+
assert(records.some(record => record.event === 'knowledge.maintenance.failed' && record.error.message === '知识文档正文为空' && record.phase === 'validating'));
|
|
33
|
+
await supervised.stopSupervised(); await logger.close();
|
|
34
|
+
console.log('module process lifecycle test PASS');
|
|
35
|
+
})().catch(error => { console.error(error); process.exitCode = 1; });
|
|
@@ -1,34 +1,34 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
const assert = require('node:assert/strict');
|
|
3
|
-
const http = require('node:http');
|
|
4
|
-
const { ModuleBridge } = require('../lib/module-bridge');
|
|
5
|
-
|
|
6
|
-
(async () => {
|
|
7
|
-
let disconnected = false; let cursor;
|
|
8
|
-
const upstream = http.createServer((req, res) => {
|
|
9
|
-
cursor = req.headers['last-event-id'];
|
|
10
|
-
res.writeHead(200, { 'Content-Type': 'text/event-stream' });
|
|
11
|
-
res.write('id: 8\nevent: delta\ndata: {"text":"first"}\n\n');
|
|
12
|
-
const timer = setInterval(() => res.write(': live\n\n'), 100);
|
|
13
|
-
res.on('close', () => { clearInterval(timer); disconnected = true; });
|
|
14
|
-
});
|
|
15
|
-
await new Promise(resolve => upstream.listen(0, '127.0.0.1', resolve));
|
|
16
|
-
const bridge = new ModuleBridge();
|
|
17
|
-
const shell = http.createServer((req, res) => bridge.proxy(req, res, `http://127.0.0.1:${upstream.address().port}/events`));
|
|
18
|
-
await new Promise(resolve => shell.listen(0, '127.0.0.1', resolve));
|
|
19
|
-
const abort = new AbortController();
|
|
20
|
-
try {
|
|
21
|
-
const response = await fetch(`http://127.0.0.1:${shell.address().port}/events`, { headers: { 'Last-Event-ID': '7' }, signal: abort.signal });
|
|
22
|
-
const reader = response.body.getReader();
|
|
23
|
-
const first = await Promise.race([reader.read(), new Promise((_, reject) => setTimeout(() => reject(new Error('SSE was buffered')), 1500))]);
|
|
24
|
-
assert.match(Buffer.from(first.value).toString(), /first/);
|
|
25
|
-
assert.equal(cursor, '7');
|
|
26
|
-
abort.abort();
|
|
27
|
-
for (let i = 0; i < 40 && !disconnected; i++) await new Promise(resolve => setTimeout(resolve, 25));
|
|
28
|
-
assert.equal(disconnected, true, 'browser disconnect releases upstream');
|
|
29
|
-
console.log('module-stream-proxy-test PASS');
|
|
30
|
-
} finally {
|
|
31
|
-
abort.abort();
|
|
32
|
-
for (const server of [shell, upstream]) { server.closeAllConnections(); await new Promise(resolve => server.close(resolve)); }
|
|
33
|
-
}
|
|
34
|
-
})().catch(error => { console.error(error); process.exitCode = 1; });
|
|
1
|
+
'use strict';
|
|
2
|
+
const assert = require('node:assert/strict');
|
|
3
|
+
const http = require('node:http');
|
|
4
|
+
const { ModuleBridge } = require('../lib/module-bridge');
|
|
5
|
+
|
|
6
|
+
(async () => {
|
|
7
|
+
let disconnected = false; let cursor;
|
|
8
|
+
const upstream = http.createServer((req, res) => {
|
|
9
|
+
cursor = req.headers['last-event-id'];
|
|
10
|
+
res.writeHead(200, { 'Content-Type': 'text/event-stream' });
|
|
11
|
+
res.write('id: 8\nevent: delta\ndata: {"text":"first"}\n\n');
|
|
12
|
+
const timer = setInterval(() => res.write(': live\n\n'), 100);
|
|
13
|
+
res.on('close', () => { clearInterval(timer); disconnected = true; });
|
|
14
|
+
});
|
|
15
|
+
await new Promise(resolve => upstream.listen(0, '127.0.0.1', resolve));
|
|
16
|
+
const bridge = new ModuleBridge();
|
|
17
|
+
const shell = http.createServer((req, res) => bridge.proxy(req, res, `http://127.0.0.1:${upstream.address().port}/events`));
|
|
18
|
+
await new Promise(resolve => shell.listen(0, '127.0.0.1', resolve));
|
|
19
|
+
const abort = new AbortController();
|
|
20
|
+
try {
|
|
21
|
+
const response = await fetch(`http://127.0.0.1:${shell.address().port}/events`, { headers: { 'Last-Event-ID': '7' }, signal: abort.signal });
|
|
22
|
+
const reader = response.body.getReader();
|
|
23
|
+
const first = await Promise.race([reader.read(), new Promise((_, reject) => setTimeout(() => reject(new Error('SSE was buffered')), 1500))]);
|
|
24
|
+
assert.match(Buffer.from(first.value).toString(), /first/);
|
|
25
|
+
assert.equal(cursor, '7');
|
|
26
|
+
abort.abort();
|
|
27
|
+
for (let i = 0; i < 40 && !disconnected; i++) await new Promise(resolve => setTimeout(resolve, 25));
|
|
28
|
+
assert.equal(disconnected, true, 'browser disconnect releases upstream');
|
|
29
|
+
console.log('module-stream-proxy-test PASS');
|
|
30
|
+
} finally {
|
|
31
|
+
abort.abort();
|
|
32
|
+
for (const server of [shell, upstream]) { server.closeAllConnections(); await new Promise(resolve => server.close(resolve)); }
|
|
33
|
+
}
|
|
34
|
+
})().catch(error => { console.error(error); process.exitCode = 1; });
|