@sanqianx/project-knowledge 5.0.18 → 5.0.20-rc.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.agents/plugins/marketplace.json +3 -3
- package/.claude-plugin/marketplace.json +1 -1
- package/CHANGELOG.md +14 -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-contract.js +4 -3
- 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 +8 -0
- package/_modules/knowledge-engine/src/headless-analyzer.js +2 -1
- package/_modules/knowledge-engine/src/memories.js +10 -5
- package/_modules/knowledge-engine/src/memory-eligibility.js +74 -0
- package/_modules/knowledge-engine/src/retrieval-ranking.js +19 -1
- package/_modules/knowledge-engine/src/runtime/commit-conversation-binder.js +1 -0
- package/_modules/knowledge-engine/src/runtime/commit-prompt.js +12 -9
- package/_modules/knowledge-engine/src/runtime/conversation-store.js +2 -0
- 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 -0
- package/_site/_test/candidate-product-assets-test.js +13 -0
- 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 -0
- 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 -0
- 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 -0
- 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 -0
- 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 -0
- 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 -0
- 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 +13 -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/conversation-store.js +2 -0
- package/_site/lib/data-dir.js +13 -13
- package/_site/lib/embedding-service.js +85 -85
- package/_site/lib/engine-connection.js +93 -90
- 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 -76
- package/_site/lib/llm-client.js +12 -12
- package/_site/lib/model-context-windows.js +82 -82
- package/_site/lib/module-bridge.js +874 -865
- 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 -0
- 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/server-app.js +10 -1
- 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 -72
- 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 -0
- 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 -119
- 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 -0
- package/docs/team-knowledge-mode-a-plan.md +73 -73
- package/module-runtime-manifest.json +126 -104
- package/package.json +110 -108
- 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 +175 -8
- package/vendor-manifest.json +55 -53
|
@@ -1,132 +1,132 @@
|
|
|
1
|
-
const assert = require('assert');
|
|
2
|
-
const fs = require('fs');
|
|
3
|
-
const os = require('os');
|
|
4
|
-
const path = require('path');
|
|
5
|
-
const { execFileSync } = require('child_process');
|
|
6
|
-
const { spawnServer } = require('./helpers/spawn-server');
|
|
7
|
-
const freePort = require('./helpers/free-port');
|
|
8
|
-
const githubTeamStore = require('../lib/github-team-store');
|
|
9
|
-
|
|
10
|
-
const ROOT = path.resolve(__dirname, '..', '..');
|
|
11
|
-
let PORT, BASE_URL;
|
|
12
|
-
const TEMP = fs.mkdtempSync(path.join(os.tmpdir(), `kb-team-v2-${process.pid}-`));
|
|
13
|
-
const DATA_DIR = path.join(TEMP, 'data');
|
|
14
|
-
const SOURCE_ONE = path.join(TEMP, 'source-one');
|
|
15
|
-
const SOURCE_TWO = path.join(TEMP, 'source-two');
|
|
16
|
-
const SOURCE_BAD = path.join(TEMP, 'source-bad');
|
|
17
|
-
const TEAM_STORE = path.join(TEMP, 'team-store');
|
|
18
|
-
const TEAM_REMOTE = path.join(TEMP, 'team-remote');
|
|
19
|
-
const TEAM_CLONE = path.join(TEMP, 'team-clone');
|
|
20
|
-
|
|
21
|
-
function git(cwd, args) {
|
|
22
|
-
return execFileSync('git', args, { cwd, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] }).trim();
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
function initializeRepo(cwd, title) {
|
|
26
|
-
fs.mkdirSync(cwd, { recursive: true });
|
|
27
|
-
git(cwd, ['init', '--initial-branch=main']);
|
|
28
|
-
git(cwd, ['config', 'user.name', 'Team Test']);
|
|
29
|
-
git(cwd, ['config', 'user.email', 'team@example.test']);
|
|
30
|
-
fs.writeFileSync(path.join(cwd, 'README.md'), `# ${title}\n`, 'utf8');
|
|
31
|
-
git(cwd, ['add', '.']);
|
|
32
|
-
git(cwd, ['commit', '-m', 'baseline']);
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
async function waitForServer() {
|
|
36
|
-
for (let attempt = 0; attempt < 100; attempt += 1) {
|
|
37
|
-
try { if ((await fetch(`${BASE_URL}/api/health`)).ok) return; } catch {}
|
|
38
|
-
await new Promise(resolve => setTimeout(resolve, 100));
|
|
39
|
-
}
|
|
40
|
-
throw new Error('server did not start');
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
async function json(method, route, body) {
|
|
44
|
-
const response = await fetch(`${BASE_URL}${route}`, {
|
|
45
|
-
method,
|
|
46
|
-
headers: body ? { 'Content-Type': 'application/json' } : undefined,
|
|
47
|
-
body: body ? JSON.stringify(body) : undefined,
|
|
48
|
-
});
|
|
49
|
-
const text = await response.text();
|
|
50
|
-
return { response, body: text ? JSON.parse(text) : {} };
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
(async () => {
|
|
54
|
-
PORT = process.env.KB_TEAM_KNOWLEDGE_PORT ? Number(process.env.KB_TEAM_KNOWLEDGE_PORT) : await freePort(); BASE_URL = `http://127.0.0.1:${PORT}`;
|
|
55
|
-
let spawned;
|
|
56
|
-
try {
|
|
57
|
-
initializeRepo(SOURCE_ONE, 'Source one');
|
|
58
|
-
initializeRepo(SOURCE_TWO, 'Source two');
|
|
59
|
-
initializeRepo(SOURCE_BAD, 'Source bad');
|
|
60
|
-
initializeRepo(TEAM_STORE, 'Team store');
|
|
61
|
-
fs.mkdirSync(path.join(TEAM_STORE, 'acc'), { recursive: true });
|
|
62
|
-
fs.writeFileSync(path.join(TEAM_STORE, 'acc', 'README.md'), '# ACC shared knowledge\n', 'utf8');
|
|
63
|
-
git(TEAM_STORE, ['add', '.']);
|
|
64
|
-
git(TEAM_STORE, ['commit', '-m', 'add team knowledge']);
|
|
65
|
-
git(TEAM_STORE, ['remote', 'add', 'origin', TEAM_STORE]);
|
|
66
|
-
|
|
67
|
-
initializeRepo(TEAM_REMOTE, 'Remote team store');
|
|
68
|
-
fs.mkdirSync(path.join(TEAM_REMOTE, 'acc'), { recursive: true });
|
|
69
|
-
fs.writeFileSync(path.join(TEAM_REMOTE, 'acc', 'README.md'), '# ACC from remote\n', 'utf8');
|
|
70
|
-
git(TEAM_REMOTE, ['add', '.']);
|
|
71
|
-
git(TEAM_REMOTE, ['commit', '-m', 'add remote knowledge']);
|
|
72
|
-
|
|
73
|
-
const providerConfigPath = path.join(TEMP, 'providers.json');
|
|
74
|
-
fs.writeFileSync(providerConfigPath, `\uFEFF${JSON.stringify({ gitea: { webBaseUrl: 'https://gitea.example.test', oauthClientId: 'client', oauthClientSecret: 'secret' } })}`, 'utf8');
|
|
75
|
-
const providerFile = githubTeamStore.readProviderFileConfig(providerConfigPath);
|
|
76
|
-
const publicProviders = githubTeamStore.providerPublicConfig(githubTeamStore.defaultConfig(), {}, providerFile);
|
|
77
|
-
assert.strictEqual(publicProviders.gitea.configured, true);
|
|
78
|
-
assert.strictEqual(publicProviders.gitea.oauthClientSecret, undefined);
|
|
79
|
-
assert.strictEqual(publicProviders.gitea.oauthClientSecretConfigured, true);
|
|
80
|
-
assert.strictEqual(githubTeamStore.proxyUrlForTarget('http://127.0.0.1:3000/api/v1/user', { KB_GIT_PROXY: 'http://127.0.0.1:7890' }), '');
|
|
81
|
-
|
|
82
|
-
const scanned = await githubTeamStore.scanLocalStore({ localPath: TEAM_STORE });
|
|
83
|
-
assert(scanned.ok && scanned.store.knowledgeBases.some(item => item.path === 'acc'), JSON.stringify(scanned));
|
|
84
|
-
const checkedOut = await githubTeamStore.checkoutStore({ cloneUrl: TEAM_REMOTE, branch: 'main', localPath: TEAM_CLONE });
|
|
85
|
-
assert(checkedOut.ok, JSON.stringify(checkedOut));
|
|
86
|
-
assert.match(fs.readFileSync(path.join(TEAM_CLONE, 'acc', 'README.md'), 'utf8'), /from remote/);
|
|
87
|
-
|
|
88
|
-
spawned = spawnServer({ root: ROOT, port: PORT, dataDir: DATA_DIR, tag: 'team-kb-v2' });
|
|
89
|
-
let output = '';
|
|
90
|
-
spawned.child.stdout.on('data', chunk => { output += chunk; });
|
|
91
|
-
spawned.child.stderr.on('data', chunk => { output += chunk; });
|
|
92
|
-
await waitForServer();
|
|
93
|
-
let result = await json('PATCH', '/api/settings', { knowledge: { rootPath: path.join(TEMP, 'personal-knowledge') } });
|
|
94
|
-
assert(result.response.ok, JSON.stringify(result.body));
|
|
95
|
-
|
|
96
|
-
result = await json('POST', '/api/projects/import', {
|
|
97
|
-
projectId: 'team-project-one', localPath: SOURCE_ONE,
|
|
98
|
-
teamKnowledgeBase: {
|
|
99
|
-
storeLocalPath: TEAM_STORE, storeRemoteUrl: 'https://github.com/org/knowledge.git',
|
|
100
|
-
storeFullName: 'org/knowledge', storeId: 'team-store', kbId: 'kb-acc', kbSlug: 'acc', kbSubdir: 'acc', displayName: 'ACC',
|
|
101
|
-
},
|
|
102
|
-
});
|
|
103
|
-
assert.strictEqual(result.response.status, 201, `${JSON.stringify(result.body)}\n${output}`);
|
|
104
|
-
assert.strictEqual(path.resolve(result.body.config.knowledgePath), path.resolve(TEAM_STORE, 'acc'));
|
|
105
|
-
assert.strictEqual(result.body.config.teamBinding.provider, 'github');
|
|
106
|
-
assert.strictEqual(result.body.config.teamBinding.kbId, 'kb-acc');
|
|
107
|
-
assert.strictEqual(result.body.config.teamBinding.kbSubdir, 'acc');
|
|
108
|
-
const registryText = fs.readFileSync(path.join(DATA_DIR, 'projects.json'), 'utf8');
|
|
109
|
-
assert(!registryText.includes(TEAM_STORE), 'minimal v2 registry must not contain the external team path');
|
|
110
|
-
|
|
111
|
-
result = await json('POST', '/api/projects/import', {
|
|
112
|
-
projectId: 'team-project-bad', localPath: SOURCE_BAD,
|
|
113
|
-
teamKnowledgeBase: { storeLocalPath: TEAM_STORE, kbSubdir: '../escape' },
|
|
114
|
-
});
|
|
115
|
-
assert.strictEqual(result.response.status, 403, 'team binding traversal must be rejected');
|
|
116
|
-
|
|
117
|
-
result = await json('POST', '/api/projects/import', {
|
|
118
|
-
projectId: 'team-project-two', localPath: SOURCE_TWO,
|
|
119
|
-
teamKnowledgeBase: { storeLocalPath: TEAM_CLONE, storeRemoteUrl: TEAM_REMOTE, storeId: 'clone', kbId: 'kb-acc', kbSubdir: 'acc' },
|
|
120
|
-
});
|
|
121
|
-
assert.strictEqual(result.response.status, 201, JSON.stringify(result.body));
|
|
122
|
-
assert.strictEqual(path.resolve(result.body.config.knowledgePath), path.resolve(TEAM_CLONE, 'acc'));
|
|
123
|
-
|
|
124
|
-
result = await json('DELETE', '/api/projects/team-project-one', { deleteKnowledge: true, confirmationToken: 'team-project-one' });
|
|
125
|
-
assert(result.response.ok, JSON.stringify(result.body));
|
|
126
|
-
assert.strictEqual(result.body.removedKnowledge, false, 'team knowledge is externally owned');
|
|
127
|
-
assert(fs.existsSync(path.join(TEAM_STORE, 'acc', 'README.md')), 'team knowledge must survive project deletion');
|
|
128
|
-
console.log('team knowledge store test passed');
|
|
129
|
-
} finally {
|
|
130
|
-
if (spawned) spawned.child.kill();
|
|
131
|
-
}
|
|
132
|
-
})().catch(error => { console.error(error); process.exitCode = 1; });
|
|
1
|
+
const assert = require('assert');
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const os = require('os');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const { execFileSync } = require('child_process');
|
|
6
|
+
const { spawnServer } = require('./helpers/spawn-server');
|
|
7
|
+
const freePort = require('./helpers/free-port');
|
|
8
|
+
const githubTeamStore = require('../lib/github-team-store');
|
|
9
|
+
|
|
10
|
+
const ROOT = path.resolve(__dirname, '..', '..');
|
|
11
|
+
let PORT, BASE_URL;
|
|
12
|
+
const TEMP = fs.mkdtempSync(path.join(os.tmpdir(), `kb-team-v2-${process.pid}-`));
|
|
13
|
+
const DATA_DIR = path.join(TEMP, 'data');
|
|
14
|
+
const SOURCE_ONE = path.join(TEMP, 'source-one');
|
|
15
|
+
const SOURCE_TWO = path.join(TEMP, 'source-two');
|
|
16
|
+
const SOURCE_BAD = path.join(TEMP, 'source-bad');
|
|
17
|
+
const TEAM_STORE = path.join(TEMP, 'team-store');
|
|
18
|
+
const TEAM_REMOTE = path.join(TEMP, 'team-remote');
|
|
19
|
+
const TEAM_CLONE = path.join(TEMP, 'team-clone');
|
|
20
|
+
|
|
21
|
+
function git(cwd, args) {
|
|
22
|
+
return execFileSync('git', args, { cwd, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] }).trim();
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function initializeRepo(cwd, title) {
|
|
26
|
+
fs.mkdirSync(cwd, { recursive: true });
|
|
27
|
+
git(cwd, ['init', '--initial-branch=main']);
|
|
28
|
+
git(cwd, ['config', 'user.name', 'Team Test']);
|
|
29
|
+
git(cwd, ['config', 'user.email', 'team@example.test']);
|
|
30
|
+
fs.writeFileSync(path.join(cwd, 'README.md'), `# ${title}\n`, 'utf8');
|
|
31
|
+
git(cwd, ['add', '.']);
|
|
32
|
+
git(cwd, ['commit', '-m', 'baseline']);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async function waitForServer() {
|
|
36
|
+
for (let attempt = 0; attempt < 100; attempt += 1) {
|
|
37
|
+
try { if ((await fetch(`${BASE_URL}/api/health`)).ok) return; } catch {}
|
|
38
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
39
|
+
}
|
|
40
|
+
throw new Error('server did not start');
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async function json(method, route, body) {
|
|
44
|
+
const response = await fetch(`${BASE_URL}${route}`, {
|
|
45
|
+
method,
|
|
46
|
+
headers: body ? { 'Content-Type': 'application/json' } : undefined,
|
|
47
|
+
body: body ? JSON.stringify(body) : undefined,
|
|
48
|
+
});
|
|
49
|
+
const text = await response.text();
|
|
50
|
+
return { response, body: text ? JSON.parse(text) : {} };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
(async () => {
|
|
54
|
+
PORT = process.env.KB_TEAM_KNOWLEDGE_PORT ? Number(process.env.KB_TEAM_KNOWLEDGE_PORT) : await freePort(); BASE_URL = `http://127.0.0.1:${PORT}`;
|
|
55
|
+
let spawned;
|
|
56
|
+
try {
|
|
57
|
+
initializeRepo(SOURCE_ONE, 'Source one');
|
|
58
|
+
initializeRepo(SOURCE_TWO, 'Source two');
|
|
59
|
+
initializeRepo(SOURCE_BAD, 'Source bad');
|
|
60
|
+
initializeRepo(TEAM_STORE, 'Team store');
|
|
61
|
+
fs.mkdirSync(path.join(TEAM_STORE, 'acc'), { recursive: true });
|
|
62
|
+
fs.writeFileSync(path.join(TEAM_STORE, 'acc', 'README.md'), '# ACC shared knowledge\n', 'utf8');
|
|
63
|
+
git(TEAM_STORE, ['add', '.']);
|
|
64
|
+
git(TEAM_STORE, ['commit', '-m', 'add team knowledge']);
|
|
65
|
+
git(TEAM_STORE, ['remote', 'add', 'origin', TEAM_STORE]);
|
|
66
|
+
|
|
67
|
+
initializeRepo(TEAM_REMOTE, 'Remote team store');
|
|
68
|
+
fs.mkdirSync(path.join(TEAM_REMOTE, 'acc'), { recursive: true });
|
|
69
|
+
fs.writeFileSync(path.join(TEAM_REMOTE, 'acc', 'README.md'), '# ACC from remote\n', 'utf8');
|
|
70
|
+
git(TEAM_REMOTE, ['add', '.']);
|
|
71
|
+
git(TEAM_REMOTE, ['commit', '-m', 'add remote knowledge']);
|
|
72
|
+
|
|
73
|
+
const providerConfigPath = path.join(TEMP, 'providers.json');
|
|
74
|
+
fs.writeFileSync(providerConfigPath, `\uFEFF${JSON.stringify({ gitea: { webBaseUrl: 'https://gitea.example.test', oauthClientId: 'client', oauthClientSecret: 'secret' } })}`, 'utf8');
|
|
75
|
+
const providerFile = githubTeamStore.readProviderFileConfig(providerConfigPath);
|
|
76
|
+
const publicProviders = githubTeamStore.providerPublicConfig(githubTeamStore.defaultConfig(), {}, providerFile);
|
|
77
|
+
assert.strictEqual(publicProviders.gitea.configured, true);
|
|
78
|
+
assert.strictEqual(publicProviders.gitea.oauthClientSecret, undefined);
|
|
79
|
+
assert.strictEqual(publicProviders.gitea.oauthClientSecretConfigured, true);
|
|
80
|
+
assert.strictEqual(githubTeamStore.proxyUrlForTarget('http://127.0.0.1:3000/api/v1/user', { KB_GIT_PROXY: 'http://127.0.0.1:7890' }), '');
|
|
81
|
+
|
|
82
|
+
const scanned = await githubTeamStore.scanLocalStore({ localPath: TEAM_STORE });
|
|
83
|
+
assert(scanned.ok && scanned.store.knowledgeBases.some(item => item.path === 'acc'), JSON.stringify(scanned));
|
|
84
|
+
const checkedOut = await githubTeamStore.checkoutStore({ cloneUrl: TEAM_REMOTE, branch: 'main', localPath: TEAM_CLONE });
|
|
85
|
+
assert(checkedOut.ok, JSON.stringify(checkedOut));
|
|
86
|
+
assert.match(fs.readFileSync(path.join(TEAM_CLONE, 'acc', 'README.md'), 'utf8'), /from remote/);
|
|
87
|
+
|
|
88
|
+
spawned = spawnServer({ root: ROOT, port: PORT, dataDir: DATA_DIR, tag: 'team-kb-v2' });
|
|
89
|
+
let output = '';
|
|
90
|
+
spawned.child.stdout.on('data', chunk => { output += chunk; });
|
|
91
|
+
spawned.child.stderr.on('data', chunk => { output += chunk; });
|
|
92
|
+
await waitForServer();
|
|
93
|
+
let result = await json('PATCH', '/api/settings', { knowledge: { rootPath: path.join(TEMP, 'personal-knowledge') } });
|
|
94
|
+
assert(result.response.ok, JSON.stringify(result.body));
|
|
95
|
+
|
|
96
|
+
result = await json('POST', '/api/projects/import', {
|
|
97
|
+
projectId: 'team-project-one', localPath: SOURCE_ONE,
|
|
98
|
+
teamKnowledgeBase: {
|
|
99
|
+
storeLocalPath: TEAM_STORE, storeRemoteUrl: 'https://github.com/org/knowledge.git',
|
|
100
|
+
storeFullName: 'org/knowledge', storeId: 'team-store', kbId: 'kb-acc', kbSlug: 'acc', kbSubdir: 'acc', displayName: 'ACC',
|
|
101
|
+
},
|
|
102
|
+
});
|
|
103
|
+
assert.strictEqual(result.response.status, 201, `${JSON.stringify(result.body)}\n${output}`);
|
|
104
|
+
assert.strictEqual(path.resolve(result.body.config.knowledgePath), path.resolve(TEAM_STORE, 'acc'));
|
|
105
|
+
assert.strictEqual(result.body.config.teamBinding.provider, 'github');
|
|
106
|
+
assert.strictEqual(result.body.config.teamBinding.kbId, 'kb-acc');
|
|
107
|
+
assert.strictEqual(result.body.config.teamBinding.kbSubdir, 'acc');
|
|
108
|
+
const registryText = fs.readFileSync(path.join(DATA_DIR, 'projects.json'), 'utf8');
|
|
109
|
+
assert(!registryText.includes(TEAM_STORE), 'minimal v2 registry must not contain the external team path');
|
|
110
|
+
|
|
111
|
+
result = await json('POST', '/api/projects/import', {
|
|
112
|
+
projectId: 'team-project-bad', localPath: SOURCE_BAD,
|
|
113
|
+
teamKnowledgeBase: { storeLocalPath: TEAM_STORE, kbSubdir: '../escape' },
|
|
114
|
+
});
|
|
115
|
+
assert.strictEqual(result.response.status, 403, 'team binding traversal must be rejected');
|
|
116
|
+
|
|
117
|
+
result = await json('POST', '/api/projects/import', {
|
|
118
|
+
projectId: 'team-project-two', localPath: SOURCE_TWO,
|
|
119
|
+
teamKnowledgeBase: { storeLocalPath: TEAM_CLONE, storeRemoteUrl: TEAM_REMOTE, storeId: 'clone', kbId: 'kb-acc', kbSubdir: 'acc' },
|
|
120
|
+
});
|
|
121
|
+
assert.strictEqual(result.response.status, 201, JSON.stringify(result.body));
|
|
122
|
+
assert.strictEqual(path.resolve(result.body.config.knowledgePath), path.resolve(TEAM_CLONE, 'acc'));
|
|
123
|
+
|
|
124
|
+
result = await json('DELETE', '/api/projects/team-project-one', { deleteKnowledge: true, confirmationToken: 'team-project-one' });
|
|
125
|
+
assert(result.response.ok, JSON.stringify(result.body));
|
|
126
|
+
assert.strictEqual(result.body.removedKnowledge, false, 'team knowledge is externally owned');
|
|
127
|
+
assert(fs.existsSync(path.join(TEAM_STORE, 'acc', 'README.md')), 'team knowledge must survive project deletion');
|
|
128
|
+
console.log('team knowledge store test passed');
|
|
129
|
+
} finally {
|
|
130
|
+
if (spawned) spawned.child.kill();
|
|
131
|
+
}
|
|
132
|
+
})().catch(error => { console.error(error); process.exitCode = 1; });
|
|
@@ -1,35 +1,35 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
const assert = require('node:assert/strict');
|
|
3
|
-
const fs = require('node:fs');
|
|
4
|
-
const path = require('node:path');
|
|
5
|
-
const { spawnServer } = require('./helpers/spawn-server');
|
|
6
|
-
const freePort = require('./helpers/free-port');
|
|
7
|
-
const { readEndpoint } = require('../lib/runtime-endpoint');
|
|
8
|
-
const { spawn } = require('node:child_process');
|
|
9
|
-
|
|
10
|
-
(async () => {
|
|
11
|
-
const port = await freePort();
|
|
12
|
-
const spawned = spawnServer({ root: path.resolve(__dirname, '../..'), port, tag: 'graceful-cleanup' });
|
|
13
|
-
let stopped = false;
|
|
14
|
-
try {
|
|
15
|
-
for (let attempt = 0; attempt < 100; attempt++) {
|
|
16
|
-
const endpoint = readEndpoint(spawned.dataDir);
|
|
17
|
-
if (endpoint && (await fetch(`http://127.0.0.1:${port}/api/health`).catch(() => null))?.ok) break;
|
|
18
|
-
await new Promise(resolve => setTimeout(resolve, 100));
|
|
19
|
-
}
|
|
20
|
-
assert.equal(readEndpoint(spawned.dataDir)?.pid, spawned.child.pid);
|
|
21
|
-
let output = '';
|
|
22
|
-
spawned.child.stderr.on('data', bytes => { output += bytes; });
|
|
23
|
-
const locker = process.platform === 'win32' ? spawn(process.execPath, ['-e', 'process.stdout.write("holding-cwd\\n");setTimeout(()=>process.exit(0),2500)'], { cwd: spawned.dataDir, windowsHide: true, stdio: ['ignore', 'pipe', 'pipe'] }) : null;
|
|
24
|
-
if (locker) await new Promise((resolve, reject) => { locker.once('error', reject); locker.stdout.once('data', resolve); });
|
|
25
|
-
try { await spawned.cleanup(); stopped = true; }
|
|
26
|
-
finally { if (locker?.exitCode === null) { locker.kill(); await new Promise(resolve => locker.once('close', resolve)); } }
|
|
27
|
-
assert.notEqual(spawned.child.exitCode, null, output);
|
|
28
|
-
assert.equal(spawned.child.signalCode, null, 'normal cleanup must drain the server instead of force killing it');
|
|
29
|
-
assert.equal(spawned.child.exitCode, 0, output);
|
|
30
|
-
assert.ok(!fs.existsSync(spawned.dataDir));
|
|
31
|
-
assert.ok(!fs.existsSync(spawned.bridgeHome));
|
|
32
|
-
await assert.rejects(fetch(`http://127.0.0.1:${port}/api/health`));
|
|
33
|
-
console.log('test-server-cleanup-test PASS (verified graceful shutdown before fixture removal)');
|
|
34
|
-
} finally { if (!stopped) await spawned.cleanup(); }
|
|
35
|
-
})().catch(error => { console.error(error); process.exitCode = 1; });
|
|
1
|
+
'use strict';
|
|
2
|
+
const assert = require('node:assert/strict');
|
|
3
|
+
const fs = require('node:fs');
|
|
4
|
+
const path = require('node:path');
|
|
5
|
+
const { spawnServer } = require('./helpers/spawn-server');
|
|
6
|
+
const freePort = require('./helpers/free-port');
|
|
7
|
+
const { readEndpoint } = require('../lib/runtime-endpoint');
|
|
8
|
+
const { spawn } = require('node:child_process');
|
|
9
|
+
|
|
10
|
+
(async () => {
|
|
11
|
+
const port = await freePort();
|
|
12
|
+
const spawned = spawnServer({ root: path.resolve(__dirname, '../..'), port, tag: 'graceful-cleanup' });
|
|
13
|
+
let stopped = false;
|
|
14
|
+
try {
|
|
15
|
+
for (let attempt = 0; attempt < 100; attempt++) {
|
|
16
|
+
const endpoint = readEndpoint(spawned.dataDir);
|
|
17
|
+
if (endpoint && (await fetch(`http://127.0.0.1:${port}/api/health`).catch(() => null))?.ok) break;
|
|
18
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
19
|
+
}
|
|
20
|
+
assert.equal(readEndpoint(spawned.dataDir)?.pid, spawned.child.pid);
|
|
21
|
+
let output = '';
|
|
22
|
+
spawned.child.stderr.on('data', bytes => { output += bytes; });
|
|
23
|
+
const locker = process.platform === 'win32' ? spawn(process.execPath, ['-e', 'process.stdout.write("holding-cwd\\n");setTimeout(()=>process.exit(0),2500)'], { cwd: spawned.dataDir, windowsHide: true, stdio: ['ignore', 'pipe', 'pipe'] }) : null;
|
|
24
|
+
if (locker) await new Promise((resolve, reject) => { locker.once('error', reject); locker.stdout.once('data', resolve); });
|
|
25
|
+
try { await spawned.cleanup(); stopped = true; }
|
|
26
|
+
finally { if (locker?.exitCode === null) { locker.kill(); await new Promise(resolve => locker.once('close', resolve)); } }
|
|
27
|
+
assert.notEqual(spawned.child.exitCode, null, output);
|
|
28
|
+
assert.equal(spawned.child.signalCode, null, 'normal cleanup must drain the server instead of force killing it');
|
|
29
|
+
assert.equal(spawned.child.exitCode, 0, output);
|
|
30
|
+
assert.ok(!fs.existsSync(spawned.dataDir));
|
|
31
|
+
assert.ok(!fs.existsSync(spawned.bridgeHome));
|
|
32
|
+
await assert.rejects(fetch(`http://127.0.0.1:${port}/api/health`));
|
|
33
|
+
console.log('test-server-cleanup-test PASS (verified graceful shutdown before fixture removal)');
|
|
34
|
+
} finally { if (!stopped) await spawned.cleanup(); }
|
|
35
|
+
})().catch(error => { console.error(error); process.exitCode = 1; });
|
|
@@ -1,37 +1,37 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
const assert = require('node:assert/strict');
|
|
3
|
-
const fs = require('node:fs');
|
|
4
|
-
const path = require('node:path');
|
|
5
|
-
const vm = require('node:vm');
|
|
6
|
-
|
|
7
|
-
const helper = path.join(__dirname, 'helpers/spawn-server.js');
|
|
8
|
-
const calls = [];
|
|
9
|
-
const moduleObject = { exports: {} };
|
|
10
|
-
const inherited = {
|
|
11
|
-
KB_TERMINAL_URL: 'http://127.0.0.1:5760',
|
|
12
|
-
KB_VECTORHUB_URL: 'http://127.0.0.1:8787',
|
|
13
|
-
KB_EVENTBRIDGE_URL: 'http://127.0.0.1:8790',
|
|
14
|
-
KB_KNOWLEDGE_ENGINE_URL: 'http://127.0.0.1:5790',
|
|
15
|
-
};
|
|
16
|
-
vm.runInNewContext(fs.readFileSync(helper, 'utf8'), {
|
|
17
|
-
module: moduleObject,
|
|
18
|
-
require(id) {
|
|
19
|
-
if (id === 'child_process') return { spawn: (...args) => { calls.push(args); return { pid: 1 }; } };
|
|
20
|
-
if (id === 'fs') return { mkdtempSync: prefix => prefix + 'owned' };
|
|
21
|
-
if (id === '../../lib/runtime-control' || id === '../../lib/runtime-endpoint') return {};
|
|
22
|
-
return require(id);
|
|
23
|
-
},
|
|
24
|
-
process: { execPath: process.execPath, pid: process.pid, env: inherited },
|
|
25
|
-
});
|
|
26
|
-
const spawnServer = moduleObject.exports.spawnServer;
|
|
27
|
-
spawnServer({ root: path.resolve(__dirname, '../..'), port: 0, dataDir: 'isolated-data' });
|
|
28
|
-
const defaults = calls[0][2].env;
|
|
29
|
-
for (const key of Object.keys(inherited)) assert.equal(defaults[key], 'http://127.0.0.1:0', key + ' must not consult a live user module');
|
|
30
|
-
assert.equal(defaults.KB_DATA_DIR, 'isolated-data');
|
|
31
|
-
assert.equal(defaults.KB_MODULES_AUTOSTART, '0');
|
|
32
|
-
assert.equal(defaults.KB_MODULES_REGISTER, '0');
|
|
33
|
-
assert.notEqual(defaults.AI_CODING_EVENT_BRIDGE_HOME, process.env.AI_CODING_EVENT_BRIDGE_HOME);
|
|
34
|
-
const ownUrls = Object.fromEntries(Object.keys(inherited).map(key => [key, 'http://127.0.0.1:12345']));
|
|
35
|
-
spawnServer({ root: path.resolve(__dirname, '../..'), port: 0, dataDir: 'isolated-data', extraEnv: ownUrls });
|
|
36
|
-
for (const key of Object.keys(inherited)) assert.equal(calls[1][2].env[key], ownUrls[key], 'explicit owned fixture remains supported');
|
|
37
|
-
console.log('test-server-module-isolation-test PASS');
|
|
1
|
+
'use strict';
|
|
2
|
+
const assert = require('node:assert/strict');
|
|
3
|
+
const fs = require('node:fs');
|
|
4
|
+
const path = require('node:path');
|
|
5
|
+
const vm = require('node:vm');
|
|
6
|
+
|
|
7
|
+
const helper = path.join(__dirname, 'helpers/spawn-server.js');
|
|
8
|
+
const calls = [];
|
|
9
|
+
const moduleObject = { exports: {} };
|
|
10
|
+
const inherited = {
|
|
11
|
+
KB_TERMINAL_URL: 'http://127.0.0.1:5760',
|
|
12
|
+
KB_VECTORHUB_URL: 'http://127.0.0.1:8787',
|
|
13
|
+
KB_EVENTBRIDGE_URL: 'http://127.0.0.1:8790',
|
|
14
|
+
KB_KNOWLEDGE_ENGINE_URL: 'http://127.0.0.1:5790',
|
|
15
|
+
};
|
|
16
|
+
vm.runInNewContext(fs.readFileSync(helper, 'utf8'), {
|
|
17
|
+
module: moduleObject,
|
|
18
|
+
require(id) {
|
|
19
|
+
if (id === 'child_process') return { spawn: (...args) => { calls.push(args); return { pid: 1 }; } };
|
|
20
|
+
if (id === 'fs') return { mkdtempSync: prefix => prefix + 'owned' };
|
|
21
|
+
if (id === '../../lib/runtime-control' || id === '../../lib/runtime-endpoint') return {};
|
|
22
|
+
return require(id);
|
|
23
|
+
},
|
|
24
|
+
process: { execPath: process.execPath, pid: process.pid, env: inherited },
|
|
25
|
+
});
|
|
26
|
+
const spawnServer = moduleObject.exports.spawnServer;
|
|
27
|
+
spawnServer({ root: path.resolve(__dirname, '../..'), port: 0, dataDir: 'isolated-data' });
|
|
28
|
+
const defaults = calls[0][2].env;
|
|
29
|
+
for (const key of Object.keys(inherited)) assert.equal(defaults[key], 'http://127.0.0.1:0', key + ' must not consult a live user module');
|
|
30
|
+
assert.equal(defaults.KB_DATA_DIR, 'isolated-data');
|
|
31
|
+
assert.equal(defaults.KB_MODULES_AUTOSTART, '0');
|
|
32
|
+
assert.equal(defaults.KB_MODULES_REGISTER, '0');
|
|
33
|
+
assert.notEqual(defaults.AI_CODING_EVENT_BRIDGE_HOME, process.env.AI_CODING_EVENT_BRIDGE_HOME);
|
|
34
|
+
const ownUrls = Object.fromEntries(Object.keys(inherited).map(key => [key, 'http://127.0.0.1:12345']));
|
|
35
|
+
spawnServer({ root: path.resolve(__dirname, '../..'), port: 0, dataDir: 'isolated-data', extraEnv: ownUrls });
|
|
36
|
+
for (const key of Object.keys(inherited)) assert.equal(calls[1][2].env[key], ownUrls[key], 'explicit owned fixture remains supported');
|
|
37
|
+
console.log('test-server-module-isolation-test PASS');
|
|
@@ -93,47 +93,47 @@ function commit(target, name) {
|
|
|
93
93
|
await reconciler.processCommitEvent({ projectId: 'project-existing', commitSha: nextCommit, branch: 'main' });
|
|
94
94
|
assert.strictEqual(projects.readState('project-existing').lastAnalyzedCommit, nextCommit);
|
|
95
95
|
assert.strictEqual(processorCalls, 1);
|
|
96
|
-
await reconciler.processCommitEvent({ projectId: 'project-existing', commitSha: laterCommit, branch: 'main' });
|
|
97
|
-
assert.strictEqual(projects.readState('project-existing').lastAnalyzedCommit, laterCommit, 'an event SHA remains valid even after HEAD advances');
|
|
98
|
-
assert.strictEqual(processorCalls, 2);
|
|
99
|
-
|
|
100
|
-
const terminalFailedCommit = 'a'.repeat(40);
|
|
101
|
-
await projects.updateState('project-existing', state => {
|
|
102
|
-
state.analysis.activeClaim = {
|
|
103
|
-
schema: 'commit-claim/v1', projectId: 'project-existing', commitSha: terminalFailedCommit, parents: [], requirementIds: [], requirementBinding: 'unavailable',
|
|
104
|
-
promptTemplateVersion: 'v1', promptHash: 'prompt', patchHash: 'patch', evidenceHash: 'evidence', evidenceManifestHash: 'manifest', retrievalManifestHash: 'retrieval', knowledgePath: knowledgeRoot,
|
|
105
|
-
runId: 'failed-run', operationId: 'failed-operation', phase: 'failed', attempt: 1, fingerprint: 'failed-fingerprint', error: { code: 'EPERM', retryable: false },
|
|
106
|
-
};
|
|
107
|
-
state.analysis.status = 'failed';
|
|
108
|
-
});
|
|
109
|
-
const explicitAfterFailure = commit(existingRepo, 'after-terminal-failure');
|
|
110
|
-
await reconciler.processCommitEvent({ projectId: 'project-existing', commitSha: explicitAfterFailure, branch: 'main' });
|
|
111
|
-
assert.strictEqual(projects.readState('project-existing').lastAnalyzedCommit, explicitAfterFailure, 'only the new explicit Hook SHA may proceed after a terminal failure');
|
|
112
|
-
assert.strictEqual(processorCalls, 3, 'terminal failed claims must not retry their old SHA');
|
|
113
|
-
|
|
114
|
-
const retryableFailedCommit = 'b'.repeat(40);
|
|
115
|
-
await projects.updateState('project-existing', state => {
|
|
116
|
-
state.analysis.activeClaim = {
|
|
117
|
-
schema: 'commit-claim/v1', projectId: 'project-existing', commitSha: retryableFailedCommit, parents: [], requirementIds: [], requirementBinding: 'unavailable',
|
|
118
|
-
promptTemplateVersion: 'v1', promptHash: 'prompt', patchHash: 'patch', evidenceHash: 'evidence', evidenceManifestHash: 'manifest', retrievalManifestHash: 'retrieval', knowledgePath: knowledgeRoot,
|
|
119
|
-
runId: 'retryable-run', operationId: 'retryable-operation', phase: 'failed', attempt: 1, fingerprint: 'retryable-fingerprint', error: { code: 'EAGAIN', retryable: true },
|
|
120
|
-
};
|
|
121
|
-
state.analysis.status = 'failed';
|
|
122
|
-
});
|
|
123
|
-
const blockedByRetryable = commit(existingRepo, 'after-retryable-failure');
|
|
124
|
-
const blockedResult = await reconciler.processCommitEvent({ projectId: 'project-existing', commitSha: blockedByRetryable, branch: 'main' });
|
|
125
|
-
assert.strictEqual(blockedResult.status, 'failed');
|
|
126
|
-
assert.strictEqual(processorCalls, 3, 'retryable claims must remain blocked instead of being silently superseded');
|
|
127
|
-
assert.strictEqual(projects.readState('project-existing').analysis.activeClaim.commitSha, retryableFailedCommit);
|
|
96
|
+
await reconciler.processCommitEvent({ projectId: 'project-existing', commitSha: laterCommit, branch: 'main' });
|
|
97
|
+
assert.strictEqual(projects.readState('project-existing').lastAnalyzedCommit, laterCommit, 'an event SHA remains valid even after HEAD advances');
|
|
98
|
+
assert.strictEqual(processorCalls, 2);
|
|
99
|
+
|
|
100
|
+
const terminalFailedCommit = 'a'.repeat(40);
|
|
101
|
+
await projects.updateState('project-existing', state => {
|
|
102
|
+
state.analysis.activeClaim = {
|
|
103
|
+
schema: 'commit-claim/v1', projectId: 'project-existing', commitSha: terminalFailedCommit, parents: [], requirementIds: [], requirementBinding: 'unavailable',
|
|
104
|
+
promptTemplateVersion: 'v1', promptHash: 'prompt', patchHash: 'patch', evidenceHash: 'evidence', evidenceManifestHash: 'manifest', retrievalManifestHash: 'retrieval', knowledgePath: knowledgeRoot,
|
|
105
|
+
runId: 'failed-run', operationId: 'failed-operation', phase: 'failed', attempt: 1, fingerprint: 'failed-fingerprint', error: { code: 'EPERM', retryable: false },
|
|
106
|
+
};
|
|
107
|
+
state.analysis.status = 'failed';
|
|
108
|
+
});
|
|
109
|
+
const explicitAfterFailure = commit(existingRepo, 'after-terminal-failure');
|
|
110
|
+
await reconciler.processCommitEvent({ projectId: 'project-existing', commitSha: explicitAfterFailure, branch: 'main' });
|
|
111
|
+
assert.strictEqual(projects.readState('project-existing').lastAnalyzedCommit, explicitAfterFailure, 'only the new explicit Hook SHA may proceed after a terminal failure');
|
|
112
|
+
assert.strictEqual(processorCalls, 3, 'terminal failed claims must not retry their old SHA');
|
|
113
|
+
|
|
114
|
+
const retryableFailedCommit = 'b'.repeat(40);
|
|
115
|
+
await projects.updateState('project-existing', state => {
|
|
116
|
+
state.analysis.activeClaim = {
|
|
117
|
+
schema: 'commit-claim/v1', projectId: 'project-existing', commitSha: retryableFailedCommit, parents: [], requirementIds: [], requirementBinding: 'unavailable',
|
|
118
|
+
promptTemplateVersion: 'v1', promptHash: 'prompt', patchHash: 'patch', evidenceHash: 'evidence', evidenceManifestHash: 'manifest', retrievalManifestHash: 'retrieval', knowledgePath: knowledgeRoot,
|
|
119
|
+
runId: 'retryable-run', operationId: 'retryable-operation', phase: 'failed', attempt: 1, fingerprint: 'retryable-fingerprint', error: { code: 'EAGAIN', retryable: true },
|
|
120
|
+
};
|
|
121
|
+
state.analysis.status = 'failed';
|
|
122
|
+
});
|
|
123
|
+
const blockedByRetryable = commit(existingRepo, 'after-retryable-failure');
|
|
124
|
+
const blockedResult = await reconciler.processCommitEvent({ projectId: 'project-existing', commitSha: blockedByRetryable, branch: 'main' });
|
|
125
|
+
assert.strictEqual(blockedResult.status, 'failed');
|
|
126
|
+
assert.strictEqual(processorCalls, 3, 'retryable claims must remain blocked instead of being silently superseded');
|
|
127
|
+
assert.strictEqual(projects.readState('project-existing').analysis.activeClaim.commitSha, retryableFailedCommit);
|
|
128
128
|
|
|
129
129
|
const emptyRepo = repo('empty-repo', false);
|
|
130
130
|
const emptyImported = await lifecycle.importProject({ localPath: emptyRepo, projectId: 'project-empty', aiProfileId: 'test-profile' });
|
|
131
131
|
assert.strictEqual(emptyImported.state.trackingMode, 'empty-repo');
|
|
132
132
|
assert.strictEqual(emptyImported.state.trackingStartCommit, null);
|
|
133
133
|
const firstCommit = commit(emptyRepo, 'first');
|
|
134
|
-
await reconciler.processCommitEvent({ projectId: 'project-empty', commitSha: firstCommit, branch: 'main' });
|
|
135
|
-
assert.strictEqual(projects.readState('project-empty').lastAnalyzedCommit, firstCommit, 'first commit after empty import must be analyzed');
|
|
136
|
-
assert.strictEqual(processorCalls, 4);
|
|
134
|
+
await reconciler.processCommitEvent({ projectId: 'project-empty', commitSha: firstCommit, branch: 'main' });
|
|
135
|
+
assert.strictEqual(projects.readState('project-empty').lastAnalyzedCommit, firstCommit, 'first commit after empty import must be analyzed');
|
|
136
|
+
assert.strictEqual(processorCalls, 4);
|
|
137
137
|
|
|
138
138
|
console.log('tracking-start-test PASS');
|
|
139
139
|
})().catch(error => {
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
const assert = require('node:assert/strict');
|
|
3
|
+
const fs = require('node:fs');
|
|
4
|
+
const path = require('node:path');
|
|
5
|
+
const vm = require('node:vm');
|
|
6
|
+
const source = fs.readFileSync(path.join(__dirname, '../../ui/product.js'), 'utf8');
|
|
7
|
+
const code = source.slice(source.indexOf(' function frameContext('), source.indexOf(' function setView('));
|
|
8
|
+
const messages = [], frame = { ready: false }, state = { epoch: 1, scope: 'project' };
|
|
9
|
+
const project = { projectId: 'a', workspacePath: '/workspace', modules: { terminal: { projectId: 'context-a' } } };
|
|
10
|
+
const ctx = vm.createContext({ state, frames: { terminal: frame }, activeProject: () => project,
|
|
11
|
+
document: { documentElement: { dataset: { theme: 'light' } } }, frameVisible: () => true,
|
|
12
|
+
post: (_key, message) => messages.push(message) });
|
|
13
|
+
vm.runInContext(code, ctx);
|
|
14
|
+
vm.runInContext('frameContext("terminal")', ctx);
|
|
15
|
+
assert.equal(messages.length, 0);
|
|
16
|
+
frame.ready = true;
|
|
17
|
+
vm.runInContext('frameContext("terminal"); frameContext("terminal")', ctx);
|
|
18
|
+
assert.equal(messages.filter(m => m.type === 'kb:select-project').length, 1, 'identical context receipts are not repeated');
|
|
19
|
+
state.epoch++;
|
|
20
|
+
vm.runInContext('frameContext("terminal")', ctx);
|
|
21
|
+
assert.equal(messages.filter(m => m.type === 'kb:select-project').length, 2, 'a real selection still reaches the iframe');
|
|
22
|
+
console.log('workbench-context-receipts-test PASS');
|
|
@@ -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('esc(r.title)') && 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)');
|
|
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('esc(r.title)') && 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)');
|