@sanqianx/project-knowledge 4.6.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 +20 -0
- package/.claude-plugin/marketplace.json +17 -0
- package/CHANGELOG.md +780 -0
- package/LICENSE +201 -0
- package/NOTICE +5 -0
- package/README.md +266 -0
- package/_site/README.md +30 -0
- package/_site/_test/ai-profile-resolver-test.js +137 -0
- package/_site/_test/ai-profile-test.js +75 -0
- package/_site/_test/ai-vendor-presets-test.js +91 -0
- package/_site/_test/automation-queue-test.js +14 -0
- package/_site/_test/automation-ui-test.js +40 -0
- package/_site/_test/background-task-registry-test.js +43 -0
- package/_site/_test/baseline-schema-test.js +91 -0
- package/_site/_test/bin-cli-test.js +218 -0
- package/_site/_test/bridge-adapter-test.js +95 -0
- package/_site/_test/bridge-consumer-service-test.js +310 -0
- package/_site/_test/cdp-browser-failure-test.js +100 -0
- package/_site/_test/chat-claudecodeui-match-test.js +92 -0
- package/_site/_test/claude-executable-discovery-test.js +90 -0
- package/_site/_test/claude-session-lifecycle-sweep-test.js +252 -0
- package/_site/_test/claude-workbench-test.js +170 -0
- package/_site/_test/client-capture-restart-e2e-test.js +189 -0
- package/_site/_test/codex-conversation-projection-test.js +59 -0
- package/_site/_test/codex-notify-fanout-test.js +45 -0
- package/_site/_test/commit-boundary-freeze-test.js +147 -0
- package/_site/_test/commit-conversation-binding-test.js +99 -0
- package/_site/_test/commit-evidence-test.js +110 -0
- package/_site/_test/context-pack-test.js +198 -0
- package/_site/_test/conversation-api-test.js +119 -0
- package/_site/_test/conversation-store-test.js +136 -0
- package/_site/_test/cross-repo-isolation-e2e-test.js +187 -0
- package/_site/_test/data-dir-migration-test.js +77 -0
- package/_site/_test/desktop-browser-compat-test.js +28 -0
- package/_site/_test/desktop-hook-runtime-regression-test.js +204 -0
- package/_site/_test/embedding-config-test.js +32 -0
- package/_site/_test/explicit-commit-processor-test.js +37 -0
- package/_site/_test/fixtures/fake-browser-exit.js +10 -0
- package/_site/_test/fixtures/fake-browser-hang.js +10 -0
- package/_site/_test/fixtures/make-git-repos.js +89 -0
- package/_site/_test/folder-picker-output-test.js +19 -0
- package/_site/_test/full-integration-e2e-test.js +165 -0
- package/_site/_test/git-validation-test.js +81 -0
- package/_site/_test/hook-runtime-endpoint-test.js +88 -0
- package/_site/_test/hook-status-repair-api-test.js +152 -0
- package/_site/_test/hook-trigger-test.js +85 -0
- package/_site/_test/hook-worktree-test.js +42 -0
- package/_site/_test/import-preflight-api-test.js +233 -0
- package/_site/_test/import-ui-flow-test.js +171 -0
- package/_site/_test/index-builder-test.js +41 -0
- package/_site/_test/index-writer-concurrency-test.js +135 -0
- package/_site/_test/integration-adapters-test.js +243 -0
- package/_site/_test/integration-capture-setup-test.js +108 -0
- package/_site/_test/integration-surface-coverage-test.js +165 -0
- package/_site/_test/kb-framework-test.js +57 -0
- package/_site/_test/kbpath-follow-test.js +63 -0
- package/_site/_test/knowledge-db-test.js +144 -0
- package/_site/_test/knowledge-language-control-test.js +170 -0
- package/_site/_test/knowledge-maintenance-test.js +40 -0
- package/_site/_test/knowledge-migration-test.js +75 -0
- package/_site/_test/knowledge-promotion-recovery-test.js +260 -0
- package/_site/_test/knowledge-query-test.js +54 -0
- package/_site/_test/knowledge-retrieval-service-test.js +77 -0
- package/_site/_test/knowledge-scopes-test.js +37 -0
- package/_site/_test/knowledge-storage-location-test.js +28 -0
- package/_site/_test/knowledge-storage-startup-test.js +52 -0
- package/_site/_test/knowledge-store-logs-supervision-test.js +89 -0
- package/_site/_test/legacy-claude-session-alias-test.js +106 -0
- package/_site/_test/legacy-forward-compat-test.js +257 -0
- package/_site/_test/legacy-project-upgrade-e2e-test.js +363 -0
- package/_site/_test/legacy-routes-removed-test.js +47 -0
- package/_site/_test/legacy-workbench-exclusion-test.js +92 -0
- package/_site/_test/log-redaction-test.js +58 -0
- package/_site/_test/logging-api-test.js +89 -0
- package/_site/_test/logging-sse-no-gap-test.js +88 -0
- package/_site/_test/logging-ui-test.js +104 -0
- package/_site/_test/markdown-delta-overlay-test.js +85 -0
- package/_site/_test/markdown-maintenance-api-test.js +75 -0
- package/_site/_test/markdown-maintenance-test.js +11 -0
- package/_site/_test/mcp-server-test.js +149 -0
- package/_site/_test/model-context-windows-test.js +89 -0
- package/_site/_test/module-bridge-eventbridge-test.js +193 -0
- package/_site/_test/non-release-ci-test.js +34 -0
- package/_site/_test/offline-boundary-isolation-test.js +32 -0
- package/_site/_test/orphaned-claim-recovery-test.js +80 -0
- package/_site/_test/p0-data-migration-characterization-test.js +37 -0
- package/_site/_test/p0-e2e-gate-test.js +347 -0
- package/_site/_test/package-boundary-test.js +42 -0
- package/_site/_test/package-startup-test.js +56 -0
- package/_site/_test/packaged-ui-smoke-test.js +79 -0
- package/_site/_test/path-consistency-test.js +151 -0
- package/_site/_test/pending-sweep-test.js +9 -0
- package/_site/_test/post-commit-automation-test.js +87 -0
- package/_site/_test/pr-consumer-contract-test.js +55 -0
- package/_site/_test/project-control-panel-task14-test.js +63 -0
- package/_site/_test/project-delete-recovery-test.js +64 -0
- package/_site/_test/project-goal-editor-test.js +145 -0
- package/_site/_test/project-layout-v2-migration-test.js +127 -0
- package/_site/_test/project-lifecycle-transaction-test.js +106 -0
- package/_site/_test/project-remove-running-guard-test.js +79 -0
- package/_site/_test/project-store-test.js +90 -0
- package/_site/_test/prompt-settings-test.js +115 -0
- package/_site/_test/protected-architecture-gate-test.js +123 -0
- package/_site/_test/refactor-characterization-test.js +36 -0
- package/_site/_test/release-version-sync-test.js +98 -0
- package/_site/_test/requirement-binding-test.js +113 -0
- package/_site/_test/requirement-recorder-test.js +173 -0
- package/_site/_test/run-all-tests.js +156 -0
- package/_site/_test/runtime-endpoint-test.js +64 -0
- package/_site/_test/scanner-test.js +83 -0
- package/_site/_test/server-runtime-migration-safety-test.js +27 -0
- package/_site/_test/server-security-test.js +96 -0
- package/_site/_test/sessions-stream-test.js +92 -0
- package/_site/_test/shared-contracts-test.js +58 -0
- package/_site/_test/simple-import-test.js +97 -0
- package/_site/_test/startup-analysis-disabled-test.js +16 -0
- package/_site/_test/storage-foundation-test.js +78 -0
- package/_site/_test/structured-logger-test.js +101 -0
- package/_site/_test/task15-20-ui-flow-test.js +148 -0
- package/_site/_test/team-knowledge-store-sparse-checkout-test.js +196 -0
- package/_site/_test/team-knowledge-store-test.js +131 -0
- package/_site/_test/tracking-start-test.js +143 -0
- package/_site/_test/ui-i18n-toggle-test.js +114 -0
- package/_site/_test/ui-smoke-test.js +73 -0
- package/_site/_test/v4122-upgrade-data-contract-test.js +56 -0
- package/_site/_test/workbench-permission-test.js +118 -0
- package/_site/_test/workspace-ui-contract-test.js +43 -0
- package/_site/lib/ai-profile-resolver.js +78 -0
- package/_site/lib/ai-vendor-presets.js +83 -0
- package/_site/lib/ai-workspace.js +104 -0
- package/_site/lib/atomic-file.js +204 -0
- package/_site/lib/automation-config.js +140 -0
- package/_site/lib/bridge-adapter.js +256 -0
- package/_site/lib/bridge-consumer-service.js +402 -0
- package/_site/lib/claude-cli-runner.js +1599 -0
- package/_site/lib/commit-conversation-binder.js +246 -0
- package/_site/lib/commit-processing-ledger.js +27 -0
- package/_site/lib/commit-prompt.js +202 -0
- package/_site/lib/commit-reconciler.js +444 -0
- package/_site/lib/context-pack-builder.js +299 -0
- package/_site/lib/contracts.js +279 -0
- package/_site/lib/conversation-exclusions.js +204 -0
- package/_site/lib/conversation-query-service.js +161 -0
- package/_site/lib/conversation-store.js +385 -0
- package/_site/lib/data-dir.js +81 -0
- package/_site/lib/data-state-classifier.js +40 -0
- package/_site/lib/embedding-service.js +85 -0
- package/_site/lib/evidence-bundle.js +228 -0
- package/_site/lib/folder-picker-output.js +24 -0
- package/_site/lib/git-runner.js +62 -0
- package/_site/lib/github-team-store.js +1175 -0
- package/_site/lib/hook-manager.js +202 -0
- package/_site/lib/index-builder.js +157 -0
- package/_site/lib/index-service.js +173 -0
- package/_site/lib/integration-installer.js +907 -0
- package/_site/lib/kb-framework.js +191 -0
- package/_site/lib/kb-validator.js +124 -0
- package/_site/lib/knowledge-db.js +363 -0
- package/_site/lib/knowledge-promotion.js +435 -0
- package/_site/lib/knowledge-retrieval-service.js +401 -0
- package/_site/lib/knowledge-schema.js +121 -0
- package/_site/lib/knowledge-tool-runtime.js +359 -0
- package/_site/lib/legacy-data-manifest.js +26 -0
- package/_site/lib/llm-client.js +173 -0
- package/_site/lib/markdown-knowledge-indexer.js +230 -0
- package/_site/lib/migration-service.js +442 -0
- package/_site/lib/model-context-windows.js +82 -0
- package/_site/lib/module-bridge.js +496 -0
- package/_site/lib/post-commit-automation.js +141 -0
- package/_site/lib/project-lifecycle-service.js +511 -0
- package/_site/lib/project-registry-store.js +129 -0
- package/_site/lib/project-store.js +262 -0
- package/_site/lib/prompt-registry.js +148 -0
- package/_site/lib/requirement-adapters.js +10 -0
- package/_site/lib/requirement-binder.js +112 -0
- package/_site/lib/requirement-recorder.js +278 -0
- package/_site/lib/runtime-endpoint.js +160 -0
- package/_site/lib/scanner.js +298 -0
- package/_site/lib/server-app.js +1520 -0
- package/_site/lib/settings-store.js +113 -0
- package/_site/lib/storage-layout.js +162 -0
- package/_site/lib/structured-logger.js +574 -0
- package/_site/scripts/codex-notify-fanout.cjs +51 -0
- package/_site/scripts/folder-picker.ps1 +156 -0
- package/_site/scripts/hook-trigger.js +156 -0
- package/_site/scripts/sync-release-version.js +158 -0
- package/_site/server.js +12 -0
- package/_site/start.bat +26 -0
- package/_site/stop.bat +11 -0
- package/bin/project-knowledge-integrations.js +241 -0
- package/bin/project-knowledge-kb.js +65 -0
- package/bin/project-knowledge-mcp.js +195 -0
- package/bin/project-knowledge.js +439 -0
- package/claude-prompts.json +21 -0
- package/docs/README.zh-CN.md +175 -0
- package/docs/assets/architecture.svg +182 -0
- package/docs/assets/dashboard.png +0 -0
- package/docs/assets/draft-review.png +0 -0
- package/docs/assets/logo.svg +50 -0
- package/docs/assets/runs-drafts.png +0 -0
- package/docs/assets/settings.png +0 -0
- package/docs/project-registry-schema.md +33 -0
- package/docs/team-knowledge-mode-a-plan.md +73 -0
- package/docs/testing-strategy.md +33 -0
- package/package.json +100 -0
- package/plugins/project-knowledge/.claude-plugin/plugin.json +19 -0
- package/plugins/project-knowledge/.codex-plugin/plugin.json +38 -0
- package/plugins/project-knowledge/.mcp.json +14 -0
- package/plugins/project-knowledge/opencode/project-knowledge.md +3 -0
- package/plugins/project-knowledge/skills/project-knowledge/SKILL.md +28 -0
- package/plugins/project-knowledge/skills/project-knowledge/agents/openai.yaml +11 -0
- package/templates/change.md +30 -0
- package/templates/project-readme.md +52 -0
- package/ui/app.css +58 -0
- package/ui/app.js +607 -0
- package/ui/favicon.svg +38 -0
- package/ui/i18n.js +146 -0
- package/ui/index.html +162 -0
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
const assert = require('assert');
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
|
|
5
|
+
const ROOT = path.resolve(__dirname, '..', '..');
|
|
6
|
+
const read = relative => fs.readFileSync(path.join(ROOT, relative), 'utf8');
|
|
7
|
+
const serverEntry = read('_site/server.js');
|
|
8
|
+
const server = read('_site/lib/server-app.js');
|
|
9
|
+
const automation = read('_site/lib/post-commit-automation.js');
|
|
10
|
+
const reconciler = read('_site/lib/commit-reconciler.js');
|
|
11
|
+
const contracts = read('_site/lib/contracts.js');
|
|
12
|
+
const logger = read('_site/lib/structured-logger.js');
|
|
13
|
+
const ui = read('ui/index.html');
|
|
14
|
+
|
|
15
|
+
assert(serverEntry.includes("require('./lib/server-app')"), 'server entry should remain thin');
|
|
16
|
+
for (const removed of ['/automation/simulate', '/automation/init', '/hook-install', '/hook-uninstall', '/api/raw', "'Access-Control-Allow-Origin': '*'"]) {
|
|
17
|
+
assert(!server.includes(removed), `legacy server capability must be absent: ${removed}`);
|
|
18
|
+
}
|
|
19
|
+
for (const removed of ['dispatchProjectInit', 'renderProjectInitPrompt', 'DEFAULT_INIT_PROMPT_TEMPLATE', 'dispatchAutomation']) {
|
|
20
|
+
assert(!automation.includes(removed), `legacy automation symbol must be absent: ${removed}`);
|
|
21
|
+
}
|
|
22
|
+
assert.strictEqual((automation.match(/handlePostCommitEvent/g) || []).length >= 1, true);
|
|
23
|
+
assert(reconciler.includes('validateTrigger(trigger)'));
|
|
24
|
+
assert(contracts.includes("Object.freeze(['git-hook'])"), 'git-hook must be the only analysis trigger');
|
|
25
|
+
assert(!server.includes("reconcileProjectCommits(projectId, 'startup'"), 'startup must not submit commit analysis');
|
|
26
|
+
assert(!automation.includes('dispatchPendingAutomations'), 'pending startup dispatcher must be deleted');
|
|
27
|
+
assert(reconciler.includes('processCommitEvent'), 'Hook events must have an explicit commit processor');
|
|
28
|
+
assert(!reconciler.includes('this.scanner.scan('), 'knowledge analysis must not scan baseline..HEAD');
|
|
29
|
+
assert(logger.includes('LOG_LEVELS,') && logger.includes('new Set(LOG_LEVELS)'), 'the v2 logger must use the shared six-level contract');
|
|
30
|
+
assert(logger.includes('class LogRepository'));
|
|
31
|
+
assert(!ui.includes('tailwindcss.com'));
|
|
32
|
+
assert(!ui.includes('vue.global'));
|
|
33
|
+
assert.strictEqual((ui.match(/class="shell"/g) || []).length, 1, 'production must contain one full product shell');
|
|
34
|
+
assert.strictEqual((ui.match(/id="settings-logs"/g) || []).length, 1, 'production must contain one Settings logs view');
|
|
35
|
+
|
|
36
|
+
console.log('refactor-characterization-test PASS');
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
const assert = require('assert');
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const os = require('os');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const { spawnSync } = require('child_process');
|
|
6
|
+
|
|
7
|
+
const ROOT = path.resolve(__dirname, '..', '..');
|
|
8
|
+
const SCRIPT = path.join(ROOT, '_site', 'scripts', 'sync-release-version.js');
|
|
9
|
+
const TEMP = fs.mkdtempSync(path.join(os.tmpdir(), `project-knowledge-release-version-${process.pid}-`));
|
|
10
|
+
const FILES = [
|
|
11
|
+
'package.json',
|
|
12
|
+
'package-lock.json',
|
|
13
|
+
'desktop/package.json',
|
|
14
|
+
'desktop/package-lock.json',
|
|
15
|
+
'plugins/project-knowledge/.claude-plugin/plugin.json',
|
|
16
|
+
'plugins/project-knowledge/.codex-plugin/plugin.json',
|
|
17
|
+
'.claude-plugin/marketplace.json',
|
|
18
|
+
'plugins/project-knowledge/.mcp.json',
|
|
19
|
+
];
|
|
20
|
+
|
|
21
|
+
function copyFixture() {
|
|
22
|
+
for (const relative of FILES) {
|
|
23
|
+
const destination = path.join(TEMP, relative);
|
|
24
|
+
fs.mkdirSync(path.dirname(destination), { recursive: true });
|
|
25
|
+
fs.copyFileSync(path.join(ROOT, relative), destination);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function run(args) {
|
|
30
|
+
return spawnSync(process.execPath, [SCRIPT, '--root', TEMP, ...args], {
|
|
31
|
+
cwd: ROOT,
|
|
32
|
+
encoding: 'utf8',
|
|
33
|
+
windowsHide: true,
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function git(args) {
|
|
38
|
+
const result = spawnSync('git', args, { cwd: TEMP, encoding: 'utf8', windowsHide: true });
|
|
39
|
+
assert.strictEqual(result.status, 0, result.stderr || result.stdout);
|
|
40
|
+
return result;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
try {
|
|
44
|
+
copyFixture();
|
|
45
|
+
git(['init', '-q']);
|
|
46
|
+
git(['config', 'user.email', 'release-test@example.local']);
|
|
47
|
+
git(['config', 'user.name', 'Release Test']);
|
|
48
|
+
git(['add', '.']);
|
|
49
|
+
git(['commit', '-q', '-m', 'fixture']);
|
|
50
|
+
const rootPackage = path.join(TEMP, 'package.json');
|
|
51
|
+
const value = JSON.parse(fs.readFileSync(rootPackage, 'utf8'));
|
|
52
|
+
value.version = '9.8.7';
|
|
53
|
+
fs.writeFileSync(rootPackage, `${JSON.stringify(value, null, 2)}\n`, 'utf8');
|
|
54
|
+
|
|
55
|
+
const before = run(['--check']);
|
|
56
|
+
assert.notStrictEqual(before.status, 0, 'check must fail before derived version files are synchronized');
|
|
57
|
+
assert.match(before.stderr, /out of sync/);
|
|
58
|
+
|
|
59
|
+
const sync = run(['--stage']);
|
|
60
|
+
assert.strictEqual(sync.status, 0, sync.stderr || sync.stdout);
|
|
61
|
+
const check = run(['--check']);
|
|
62
|
+
assert.strictEqual(check.status, 0, check.stderr || check.stdout);
|
|
63
|
+
const matchingTag = run(['--check', '--tag', 'v9.8.7']);
|
|
64
|
+
assert.strictEqual(matchingTag.status, 0, matchingTag.stderr || matchingTag.stdout);
|
|
65
|
+
const mismatchedTag = run(['--check', '--tag', 'v9.8.8']);
|
|
66
|
+
assert.notStrictEqual(mismatchedTag.status, 0, 'check must reject a tag that does not match package.json');
|
|
67
|
+
assert.match(mismatchedTag.stderr, /must match package version/);
|
|
68
|
+
|
|
69
|
+
const claude = JSON.parse(fs.readFileSync(path.join(TEMP, 'plugins/project-knowledge/.claude-plugin/plugin.json'), 'utf8'));
|
|
70
|
+
const codex = JSON.parse(fs.readFileSync(path.join(TEMP, 'plugins/project-knowledge/.codex-plugin/plugin.json'), 'utf8'));
|
|
71
|
+
const marketplace = JSON.parse(fs.readFileSync(path.join(TEMP, '.claude-plugin/marketplace.json'), 'utf8'));
|
|
72
|
+
const mcp = JSON.parse(fs.readFileSync(path.join(TEMP, 'plugins/project-knowledge/.mcp.json'), 'utf8'));
|
|
73
|
+
const desktopLock = JSON.parse(fs.readFileSync(path.join(TEMP, 'desktop/package-lock.json'), 'utf8'));
|
|
74
|
+
assert.strictEqual(claude.version, '9.8.7');
|
|
75
|
+
assert.strictEqual(codex.version, '9.8.7');
|
|
76
|
+
assert.strictEqual(marketplace.plugins[0].version, '9.8.7');
|
|
77
|
+
assert(mcp.mcpServers['project-knowledge'].args.includes('project-knowledge@9.8.7'));
|
|
78
|
+
assert.strictEqual(desktopLock.version, '9.8.7');
|
|
79
|
+
assert.strictEqual(desktopLock.packages[''].version, '9.8.7');
|
|
80
|
+
const staged = git(['diff', '--cached', '--name-only']);
|
|
81
|
+
const stagedFiles = staged.stdout.split(/\r?\n/).filter(Boolean).sort();
|
|
82
|
+
assert.deepStrictEqual(stagedFiles, [
|
|
83
|
+
'.claude-plugin/marketplace.json',
|
|
84
|
+
'desktop/package-lock.json',
|
|
85
|
+
'desktop/package.json',
|
|
86
|
+
'package-lock.json',
|
|
87
|
+
'plugins/project-knowledge/.claude-plugin/plugin.json',
|
|
88
|
+
'plugins/project-knowledge/.codex-plugin/plugin.json',
|
|
89
|
+
'plugins/project-knowledge/.mcp.json',
|
|
90
|
+
]);
|
|
91
|
+
const scripts = JSON.parse(fs.readFileSync(path.join(ROOT, 'package.json'), 'utf8')).scripts;
|
|
92
|
+
assert.strictEqual(scripts.preversion, 'npm run release:verify');
|
|
93
|
+
assert.strictEqual(scripts.version, 'node _site/scripts/sync-release-version.js --stage');
|
|
94
|
+
assert.strictEqual(scripts.postversion, 'npm run release:verify');
|
|
95
|
+
console.log('release-version-sync-test PASS');
|
|
96
|
+
} finally {
|
|
97
|
+
fs.rmSync(TEMP, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 });
|
|
98
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
// Run: node _site/_test/requirement-binding-test.js
|
|
2
|
+
|
|
3
|
+
const assert = require('assert');
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const os = require('os');
|
|
6
|
+
const path = require('path');
|
|
7
|
+
const { SCHEMAS } = require('../lib/contracts');
|
|
8
|
+
const { StorageLayout } = require('../lib/storage-layout');
|
|
9
|
+
const { ProjectRegistryStore } = require('../lib/project-registry-store');
|
|
10
|
+
const { ProjectStore } = require('../lib/project-store');
|
|
11
|
+
const { RequirementBinder } = require('../lib/requirement-binder');
|
|
12
|
+
|
|
13
|
+
const temp = fs.mkdtempSync(path.join(os.tmpdir(), `kb-requirement-binding-${process.pid}-`));
|
|
14
|
+
const layout = new StorageLayout({ dataDir: path.join(temp, 'data') });
|
|
15
|
+
const registry = new ProjectRegistryStore({ layout });
|
|
16
|
+
const projects = new ProjectStore({ layout });
|
|
17
|
+
const commit = 'c'.repeat(40);
|
|
18
|
+
|
|
19
|
+
function record(id, projectId, sessionId, head, options = {}) {
|
|
20
|
+
return {
|
|
21
|
+
schema: SCHEMAS.requirement,
|
|
22
|
+
id,
|
|
23
|
+
ts: options.ts || '2026-08-17T00:00:00.000Z',
|
|
24
|
+
projectId,
|
|
25
|
+
client: options.client || 'codex',
|
|
26
|
+
sessionId,
|
|
27
|
+
conversationId: null,
|
|
28
|
+
branch: options.branch || 'main',
|
|
29
|
+
headAtRecord: head,
|
|
30
|
+
requirement: `Requirement ${id}`,
|
|
31
|
+
requirementHash: `sha256:${id}`,
|
|
32
|
+
explicitCommit: options.explicitCommit || null,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
(async () => {
|
|
37
|
+
await registry.initialize();
|
|
38
|
+
for (const projectId of ['project-a', 'project-b']) {
|
|
39
|
+
const repoPath = path.join(temp, projectId);
|
|
40
|
+
fs.mkdirSync(repoPath, { recursive: true });
|
|
41
|
+
await projects.create(projectId, {
|
|
42
|
+
storageName: projectId,
|
|
43
|
+
displayName: projectId,
|
|
44
|
+
repoPath,
|
|
45
|
+
knowledgePath: path.join(temp, 'knowledge', projectId),
|
|
46
|
+
});
|
|
47
|
+
await registry.add(projectId, { displayName: projectId });
|
|
48
|
+
}
|
|
49
|
+
await projects.appendRequirement('project-a', record('req-a1', 'project-a', 'session-one', 'a'.repeat(40), { ts: '2026-08-17T00:00:01.000Z' }));
|
|
50
|
+
await projects.appendRequirement('project-a', record('req-a2', 'project-a', 'session-one', 'b'.repeat(40), { ts: '2026-08-17T00:00:02.000Z' }));
|
|
51
|
+
await projects.appendRequirement('project-a', record('req-a3', 'project-a', 'session-two', 'd'.repeat(40)));
|
|
52
|
+
await projects.appendRequirement('project-a', record('req-branch', 'project-a', 'session-one', 'e'.repeat(40), { branch: 'feature' }));
|
|
53
|
+
await projects.appendRequirement('project-a', record('req-future', 'project-a', 'session-one', 'f'.repeat(40)));
|
|
54
|
+
await projects.appendRequirement('project-a', record('req-explicit-commit', 'project-a', 'session-three', '9'.repeat(40), { explicitCommit: '8'.repeat(40) }));
|
|
55
|
+
await projects.appendRequirement('project-b', record('req-other-project', 'project-b', 'session-one', 'a'.repeat(40)));
|
|
56
|
+
|
|
57
|
+
const ancestry = new Set(['a'.repeat(40), 'b'.repeat(40), 'd'.repeat(40), 'e'.repeat(40)]);
|
|
58
|
+
const binder = new RequirementBinder({
|
|
59
|
+
layout,
|
|
60
|
+
registryStore: registry,
|
|
61
|
+
projectStore: projects,
|
|
62
|
+
gitReader: { isAncestor: async (_repoPath, headAtRecord, target) => target === commit && ancestry.has(headAtRecord) },
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
const sameSession = await binder.bind({ projectId: 'project-a', commitSha: commit, branch: 'main', client: 'codex', sessionId: 'session-one' });
|
|
66
|
+
assert.deepStrictEqual(sameSession.requirementIds, ['req-a1', 'req-a2'], 'same session and ancestry should bind its ordered unclaimed sequence');
|
|
67
|
+
assert.strictEqual(sameSession.requirementBinding, 'session-ancestry');
|
|
68
|
+
|
|
69
|
+
const ambiguous = await binder.bind({ projectId: 'project-a', commitSha: commit, branch: 'main' });
|
|
70
|
+
assert.strictEqual(ambiguous.requirementBinding, 'unavailable');
|
|
71
|
+
assert.strictEqual(ambiguous.reason, 'ambiguous-sessions', 'multiple candidate sessions must never use most-recent guessing');
|
|
72
|
+
|
|
73
|
+
const wrongSession = await binder.bind({ projectId: 'project-a', commitSha: commit, branch: 'main', client: 'codex', sessionId: 'unknown' });
|
|
74
|
+
assert.strictEqual(wrongSession.reason, 'no-reliable-requirement');
|
|
75
|
+
const changedBranch = await binder.bind({ projectId: 'project-a', commitSha: commit, branch: 'release', client: 'codex', sessionId: 'session-one' });
|
|
76
|
+
assert.strictEqual(changedBranch.requirementBinding, 'unavailable', 'branch changes should lower confidence to unavailable');
|
|
77
|
+
|
|
78
|
+
const explicit = await binder.bind({ projectId: 'project-a', commitSha: commit, requirementId: 'req-a1' });
|
|
79
|
+
assert.deepStrictEqual(explicit.requirementIds, ['req-a1']);
|
|
80
|
+
assert.strictEqual(explicit.requirementBinding, 'explicit');
|
|
81
|
+
await assert.rejects(
|
|
82
|
+
binder.bind({ projectId: 'project-a', commitSha: commit, requirementId: 'req-other-project' }),
|
|
83
|
+
error => error.code === 'INVALID_ARGUMENT' && /different project/.test(error.message),
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
const unclaimed = await binder.bind({
|
|
87
|
+
projectId: 'project-a', commitSha: commit, branch: 'main', client: 'codex', sessionId: 'session-one', claimedRequirementIds: ['req-a1'],
|
|
88
|
+
});
|
|
89
|
+
assert.deepStrictEqual(unclaimed.requirementIds, ['req-a2'], 'already advanced requirements should be excluded from a later claim');
|
|
90
|
+
|
|
91
|
+
const explicitCommit = await binder.bind({ projectId: 'project-a', commitSha: '8'.repeat(40), branch: 'main' });
|
|
92
|
+
assert.deepStrictEqual(explicitCommit.requirementIds, ['req-explicit-commit']);
|
|
93
|
+
assert.strictEqual(explicitCommit.reason, 'explicit-commit');
|
|
94
|
+
|
|
95
|
+
const claim = {
|
|
96
|
+
schema: SCHEMAS.commitClaim,
|
|
97
|
+
projectId: 'project-a',
|
|
98
|
+
commitSha: commit,
|
|
99
|
+
requirementIds: ['req-a1'],
|
|
100
|
+
requirementBinding: 'session-ancestry',
|
|
101
|
+
};
|
|
102
|
+
await projects.appendRequirement('project-a', record('req-recorded-after-claim', 'project-a', 'session-one', 'a'.repeat(40), { ts: '2026-08-17T00:00:03.000Z' }));
|
|
103
|
+
const retry = await binder.bind({ projectId: 'project-a', commitSha: commit, claim });
|
|
104
|
+
assert.deepStrictEqual(retry.requirementIds, ['req-a1'], 'retry must reuse frozen IDs and ignore future records');
|
|
105
|
+
assert.strictEqual(retry.frozen, true);
|
|
106
|
+
|
|
107
|
+
console.log('requirement-binding-test PASS');
|
|
108
|
+
})().catch(error => {
|
|
109
|
+
console.error(error.stack || error.message);
|
|
110
|
+
process.exitCode = 1;
|
|
111
|
+
}).finally(() => {
|
|
112
|
+
fs.rmSync(temp, { recursive: true, force: true });
|
|
113
|
+
});
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
// Run: node _site/_test/requirement-recorder-test.js
|
|
2
|
+
|
|
3
|
+
const assert = require('assert');
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const os = require('os');
|
|
6
|
+
const path = require('path');
|
|
7
|
+
const { spawnSync } = require('child_process');
|
|
8
|
+
const { FIELD_LIMITS, SCHEMAS, DomainError } = require('../lib/contracts');
|
|
9
|
+
const { StorageLayout } = require('../lib/storage-layout');
|
|
10
|
+
const { ProjectRegistryStore } = require('../lib/project-registry-store');
|
|
11
|
+
const { ProjectStore } = require('../lib/project-store');
|
|
12
|
+
const { RequirementRecorder } = require('../lib/requirement-recorder');
|
|
13
|
+
const { ConversationStore } = require('../lib/conversation-store');
|
|
14
|
+
const { createRequirementMetadataAdapter } = require('../lib/requirement-adapters');
|
|
15
|
+
const { KnowledgeToolRuntime } = require('../lib/knowledge-tool-runtime');
|
|
16
|
+
const claudeRunner = require('../lib/claude-cli-runner');
|
|
17
|
+
|
|
18
|
+
const temp = fs.mkdtempSync(path.join(os.tmpdir(), `kb-requirement-recorder-${process.pid}-`));
|
|
19
|
+
const dataDir = path.join(temp, 'data');
|
|
20
|
+
|
|
21
|
+
function git(repo, args) {
|
|
22
|
+
const result = spawnSync('git', ['-C', repo, ...args], { encoding: 'utf8', windowsHide: true });
|
|
23
|
+
if (result.status !== 0) throw new Error(result.stderr || `git ${args.join(' ')} failed`);
|
|
24
|
+
return String(result.stdout || '').trim();
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function createRepo(name) {
|
|
28
|
+
const repo = path.join(temp, name);
|
|
29
|
+
fs.mkdirSync(repo, { recursive: true });
|
|
30
|
+
git(repo, ['init']);
|
|
31
|
+
git(repo, ['config', 'user.email', 'requirements@example.test']);
|
|
32
|
+
git(repo, ['config', 'user.name', 'Requirement Test']);
|
|
33
|
+
fs.writeFileSync(path.join(repo, 'README.md'), `# ${name}\n`, 'utf8');
|
|
34
|
+
git(repo, ['add', 'README.md']);
|
|
35
|
+
git(repo, ['commit', '-m', 'initial']);
|
|
36
|
+
return repo;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async function addProject(registry, projects, projectId, repoPath) {
|
|
40
|
+
await projects.create(projectId, {
|
|
41
|
+
displayName: projectId,
|
|
42
|
+
storageName: projectId,
|
|
43
|
+
repoPath,
|
|
44
|
+
knowledgePath: path.join(temp, 'knowledge', projectId),
|
|
45
|
+
}, { trackingStartCommit: git(repoPath, ['rev-parse', 'HEAD']) });
|
|
46
|
+
await registry.add(projectId, { displayName: projectId });
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
(async () => {
|
|
50
|
+
const layout = new StorageLayout({ dataDir });
|
|
51
|
+
const registry = new ProjectRegistryStore({ layout });
|
|
52
|
+
const projects = new ProjectStore({ layout });
|
|
53
|
+
await registry.initialize();
|
|
54
|
+
const repoA = createRepo('repo-a');
|
|
55
|
+
const repoB = createRepo('repo-b');
|
|
56
|
+
await addProject(registry, projects, 'project-a', repoA);
|
|
57
|
+
await addProject(registry, projects, 'project-b', repoB);
|
|
58
|
+
|
|
59
|
+
const recorder = new RequirementRecorder({ layout, registryStore: registry, projectStore: projects });
|
|
60
|
+
const conversations = new ConversationStore({ layout, projectStore: projects });
|
|
61
|
+
assert.strictEqual(fs.existsSync(layout.getProjectRequirementsPath('project-a')), false, 'requirements should be lazy-created');
|
|
62
|
+
assert.strictEqual(fs.existsSync(layout.getProjectRequirementsPath('project-b')), false, 'unused project should not have an empty requirements file');
|
|
63
|
+
|
|
64
|
+
const clients = ['claude', 'codex', 'opencode'];
|
|
65
|
+
const records = [];
|
|
66
|
+
for (const client of clients) {
|
|
67
|
+
const adapter = createRequirementMetadataAdapter(recorder, client);
|
|
68
|
+
records.push(await adapter({
|
|
69
|
+
projectId: 'project-a',
|
|
70
|
+
repoPath: repoA,
|
|
71
|
+
sessionId: `session-${client}`,
|
|
72
|
+
conversationId: `conversation-${client}`,
|
|
73
|
+
text: `Implement API key validation for ${client}.`,
|
|
74
|
+
apiKey: 'must-not-be-copied-from-metadata',
|
|
75
|
+
}));
|
|
76
|
+
}
|
|
77
|
+
const schemaKeys = Object.keys(records[0]).sort();
|
|
78
|
+
for (const record of records) {
|
|
79
|
+
assert.strictEqual(record.schema, SCHEMAS.requirement);
|
|
80
|
+
assert.deepStrictEqual(Object.keys(record).sort(), schemaKeys, 'all clients should produce the same requirement schema');
|
|
81
|
+
assert.strictEqual(record.projectId, 'project-a');
|
|
82
|
+
assert(record.requirementHash.startsWith('sha256:'), 'record should include a body hash');
|
|
83
|
+
assert(record.headAtRecord && record.branch, 'trusted Git metadata should be captured when available');
|
|
84
|
+
}
|
|
85
|
+
const serialized = fs.readFileSync(layout.getProjectConversationEventsPath('project-a'), 'utf8');
|
|
86
|
+
assert(!serialized.includes('must-not-be-copied-from-metadata'), 'credential-like adapter metadata must not be persisted');
|
|
87
|
+
assert.strictEqual(fs.existsSync(layout.getProjectRequirementsPath('project-a')), false, 'new explicit adapters must not create a second requirement store');
|
|
88
|
+
assert.strictEqual(fs.existsSync(layout.getProjectRequirementsPath('project-b')), false, 'recording project A must not touch project B');
|
|
89
|
+
assert.strictEqual(projects.readState('project-a').lastAnalyzedCommit, null, 'recording must not dispatch or advance analysis');
|
|
90
|
+
assert.strictEqual(fs.existsSync(path.join(temp, 'knowledge', 'project-a')), false, 'recording must not create knowledge content');
|
|
91
|
+
|
|
92
|
+
const resolvedByRoot = await recorder.recordRequirement({
|
|
93
|
+
repoPath: path.join(repoA, 'README.md'),
|
|
94
|
+
client: 'codex',
|
|
95
|
+
sessionId: 'root-resolved',
|
|
96
|
+
text: 'Resolve only from the current Git root.',
|
|
97
|
+
});
|
|
98
|
+
assert.strictEqual(resolvedByRoot.projectId, 'project-a');
|
|
99
|
+
await assert.rejects(
|
|
100
|
+
recorder.recordRequirement({ projectId: 'project-a', repoPath: repoB, client: 'codex', sessionId: 'wrong-root', text: 'Wrong repository.' }),
|
|
101
|
+
error => error.code === 'PROJECT_NOT_FOUND',
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
const concurrent = await Promise.all(Array.from({ length: 32 }, (_, index) => recorder.recordRequirement({
|
|
105
|
+
projectId: 'project-a',
|
|
106
|
+
client: 'codex',
|
|
107
|
+
sessionId: 'concurrent-session',
|
|
108
|
+
text: `Concurrent requirement ${index}`,
|
|
109
|
+
})));
|
|
110
|
+
assert.strictEqual(new Set(concurrent.map(item => item.id)).size, 32, 'concurrent records should have unique ids');
|
|
111
|
+
assert.strictEqual(conversations.readEvents('project-a').length, 36, 'every concurrent append should remain parseable in ConversationStore');
|
|
112
|
+
|
|
113
|
+
await assert.rejects(
|
|
114
|
+
recorder.recordRequirement({ projectId: 'project-a', client: 'codex', sessionId: 'oversized', text: 'x'.repeat(FIELD_LIMITS.requirement + 1) }),
|
|
115
|
+
error => error.code === 'INVALID_ARGUMENT' && /exceeds/.test(error.message),
|
|
116
|
+
);
|
|
117
|
+
|
|
118
|
+
// T14 / I-02: the Workbench itself never appends Development Conversation
|
|
119
|
+
// events; explicit record_requirement keeps working for Workbench sessions.
|
|
120
|
+
const chat = claudeRunner.startChatSession({
|
|
121
|
+
slug: 'project-a', projectPath: repoA, kbPath: path.join(temp, 'knowledge', 'project-a'),
|
|
122
|
+
aiProfile: { id: 'test-profile', implementation: 'claude-code-agent', mainModel: 'test-model' },
|
|
123
|
+
});
|
|
124
|
+
// T15: every internal SDK session carries the Bridge capture-disable markers.
|
|
125
|
+
const chatEnv = claudeRunner.getSession(chat.sessionId).claudeEnv || {};
|
|
126
|
+
assert.strictEqual(chatEnv.AI_CODING_EVENT_BRIDGE_CAPTURE, '0', 'internal SDK sessions disable Bridge capture');
|
|
127
|
+
assert.strictEqual(chatEnv.AI_CODING_EVENT_ORIGIN, 'project-knowledge-internal', 'internal SDK sessions are marked as Project-Knowledge internal');
|
|
128
|
+
const beforeWorkbench = conversations.readEvents('project-a').length;
|
|
129
|
+
const explicitRecord = await recorder.recordRequirement({
|
|
130
|
+
projectId: 'project-a', repoPath: repoA, client: 'claude', sessionId: chat.sessionId, text: 'Explicit MCP requirement for a Workbench session.',
|
|
131
|
+
});
|
|
132
|
+
const explicitUser = conversations.readEvents('project-a').find(event => event.legacyRequirementId === explicitRecord.id);
|
|
133
|
+
assert(explicitUser, 'explicit record_requirement still persists for Workbench sessions');
|
|
134
|
+
for (const listener of claudeRunner.getSession(chat.sessionId).listeners) listener({ type: 'claude/result', result: 'Workbench reply must NOT be captured.', isError: false });
|
|
135
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
136
|
+
const afterWorkbench = conversations.readEvents('project-a');
|
|
137
|
+
assert.strictEqual(afterWorkbench.length, beforeWorkbench + 1, 'only the explicit requirement was appended');
|
|
138
|
+
assert(!afterWorkbench.some(event => String(event.content || '').includes('Workbench reply must NOT be captured.')), 'no embedded assistant capture exists');
|
|
139
|
+
assert(!afterWorkbench.some(event => event.rawEventType === 'embedded-claude-result'), 'the embedded capture path is gone');
|
|
140
|
+
claudeRunner.deleteSession(chat.sessionId);
|
|
141
|
+
|
|
142
|
+
const failingRecorder = new RequirementRecorder({
|
|
143
|
+
layout,
|
|
144
|
+
projectStore: projects,
|
|
145
|
+
conversationStore: { appendEvent: async () => { throw new Error('disk full'); } },
|
|
146
|
+
resolver: { resolve: () => ({ projectId: 'project-a', config: { repoPath: repoA } }) },
|
|
147
|
+
gitReader: { currentContext: () => ({ branch: null, headAtRecord: null }) },
|
|
148
|
+
});
|
|
149
|
+
await assert.rejects(
|
|
150
|
+
failingRecorder.recordRequirement({ projectId: 'project-a', client: 'codex', sessionId: 'failure', text: 'Durability failure.', operationId: 'op-test-failure' }),
|
|
151
|
+
error => error.operationId === 'op-test-failure' && error.retryable === true,
|
|
152
|
+
);
|
|
153
|
+
|
|
154
|
+
const runtime = new KnowledgeToolRuntime({ dataDir, cwd: repoA });
|
|
155
|
+
const toolResult = await runtime.recordRequirement({
|
|
156
|
+
projectId: 'project-b',
|
|
157
|
+
repoPath: repoB,
|
|
158
|
+
client: 'opencode',
|
|
159
|
+
sessionId: 'mcp-session',
|
|
160
|
+
text: 'MCP metadata write only.',
|
|
161
|
+
});
|
|
162
|
+
assert(toolResult.requirementId && !Object.hasOwn(toolResult, 'requirement'), 'MCP result should return an id without echoing private text');
|
|
163
|
+
assert.strictEqual(conversations.readEvents('project-b').length, 1, 'MCP runtime should append to the shared ConversationStore');
|
|
164
|
+
assert.strictEqual(fs.existsSync(layout.getProjectRequirementsPath('project-b')), false, 'MCP must not create legacy requirements.jsonl');
|
|
165
|
+
await runtime.close();
|
|
166
|
+
|
|
167
|
+
console.log('requirement-recorder-test PASS');
|
|
168
|
+
})().catch(error => {
|
|
169
|
+
console.error(error.stack || error.message);
|
|
170
|
+
process.exitCode = 1;
|
|
171
|
+
}).finally(() => {
|
|
172
|
+
fs.rmSync(temp, { recursive: true, force: true });
|
|
173
|
+
});
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
// _site/_test/run-all-tests.js
|
|
2
|
+
//
|
|
3
|
+
// Single-command regression suite runner. Iterates the canonical test files
|
|
4
|
+
// under _site/_test/ (everything matching *-test.js) and runs each one in a
|
|
5
|
+
// separate child process, capturing exit code, duration, and the last few
|
|
6
|
+
// lines of output. Writes a Markdown report to TEST-REPORT.md.
|
|
7
|
+
//
|
|
8
|
+
// Usage:
|
|
9
|
+
// node _site/_test/run-all-tests.js # run all *-test.js
|
|
10
|
+
// node _site/_test/run-all-tests.js --no-report # do not write report
|
|
11
|
+
//
|
|
12
|
+
// Exit code is 0 only if every test passes.
|
|
13
|
+
|
|
14
|
+
const fs = require('fs');
|
|
15
|
+
const os = require('os');
|
|
16
|
+
const path = require('path');
|
|
17
|
+
const { spawnSync } = require('child_process');
|
|
18
|
+
|
|
19
|
+
const ROOT = path.resolve(__dirname, '..', '..');
|
|
20
|
+
const TEST_DIR = __dirname;
|
|
21
|
+
const REPORT_PATH = path.join(TEST_DIR, 'TEST-REPORT.md');
|
|
22
|
+
|
|
23
|
+
const args = process.argv.slice(2);
|
|
24
|
+
const WRITE_REPORT = !args.includes('--no-report');
|
|
25
|
+
const PER_TEST_TIMEOUT_MS = Number(process.env.PK_TEST_TIMEOUT_MS || 90_000);
|
|
26
|
+
|
|
27
|
+
function listTestFiles() {
|
|
28
|
+
return fs.readdirSync(TEST_DIR)
|
|
29
|
+
.filter(file => file.endsWith('-test.js'))
|
|
30
|
+
.filter(file => file !== 'run-all-tests.js')
|
|
31
|
+
.sort();
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function tail(text, lines = 12) {
|
|
35
|
+
const arr = String(text || '').split(/\r?\n/).filter(Boolean);
|
|
36
|
+
return arr.slice(-lines).join('\n');
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function classifyFailure(result) {
|
|
40
|
+
if (result.status !== null) {
|
|
41
|
+
return { kind: 'exit', label: `FAIL (exit ${result.status})` };
|
|
42
|
+
}
|
|
43
|
+
if (result.error && result.error.code === 'ETIMEDOUT') {
|
|
44
|
+
return { kind: 'timeout', label: `TIMEOUT (killed after ${PER_TEST_TIMEOUT_MS}ms, signal ${result.signal || 'none'})` };
|
|
45
|
+
}
|
|
46
|
+
const code = result.error && result.error.code ? result.error.code : 'unknown';
|
|
47
|
+
return { kind: 'error', label: `FAIL (spawn error ${code}, signal ${result.signal || 'none'})` };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function runOne(file) {
|
|
51
|
+
const start = Date.now();
|
|
52
|
+
// Every test child gets its own Bridge journal sandbox so hook-trigger /
|
|
53
|
+
// server runtimes never touch the developer's ~/.ai-coding-event-bridge.
|
|
54
|
+
const bridgeHome = fs.mkdtempSync(path.join(os.tmpdir(), `kb-runall-bridge-${process.pid}-`));
|
|
55
|
+
const result = spawnSync(process.execPath, [path.join(TEST_DIR, file)], {
|
|
56
|
+
cwd: ROOT,
|
|
57
|
+
encoding: 'utf-8',
|
|
58
|
+
timeout: PER_TEST_TIMEOUT_MS,
|
|
59
|
+
env: { ...process.env, FORCE_COLOR: '0', AI_CODING_EVENT_BRIDGE_HOME: bridgeHome, KB_MODULES_AUTOSTART: '0' },
|
|
60
|
+
});
|
|
61
|
+
try { fs.rmSync(bridgeHome, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 }); } catch {}
|
|
62
|
+
const failure = result.status === 0 ? null : classifyFailure(result);
|
|
63
|
+
return {
|
|
64
|
+
file,
|
|
65
|
+
passed: result.status === 0,
|
|
66
|
+
failureKind: failure ? failure.kind : null,
|
|
67
|
+
failureLabel: failure ? failure.label : null,
|
|
68
|
+
exitCode: result.status,
|
|
69
|
+
signal: result.signal,
|
|
70
|
+
spawnErrorCode: result.error && result.error.code ? result.error.code : null,
|
|
71
|
+
durationMs: Date.now() - start,
|
|
72
|
+
stdout: result.stdout || '',
|
|
73
|
+
stderr: result.stderr || '',
|
|
74
|
+
outputTail: tail(`${result.stdout || ''}\n${result.stderr || ''}`.trim()),
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
(async () => {
|
|
79
|
+
const files = listTestFiles();
|
|
80
|
+
console.log(`Running ${files.length} test file(s)...\n`);
|
|
81
|
+
|
|
82
|
+
const results = [];
|
|
83
|
+
let totalDuration = 0;
|
|
84
|
+
for (const file of files) {
|
|
85
|
+
process.stdout.write(`> ${file} ... `);
|
|
86
|
+
const result = runOne(file);
|
|
87
|
+
totalDuration += result.durationMs;
|
|
88
|
+
results.push(result);
|
|
89
|
+
|
|
90
|
+
if (result.passed) {
|
|
91
|
+
console.log(`PASS (${result.durationMs}ms)`);
|
|
92
|
+
} else {
|
|
93
|
+
console.log(`${result.failureLabel}, ${result.durationMs}ms`);
|
|
94
|
+
if (result.outputTail) {
|
|
95
|
+
console.log('--- tail ---');
|
|
96
|
+
console.log(result.outputTail);
|
|
97
|
+
console.log('--- end tail ---');
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const passed = results.filter(result => result.passed).length;
|
|
103
|
+
const failed = results.length - passed;
|
|
104
|
+
console.log('\n=========================================');
|
|
105
|
+
console.log(`Regression suite: ${passed} passed, ${failed} failed (${totalDuration}ms total)`);
|
|
106
|
+
console.log('=========================================\n');
|
|
107
|
+
|
|
108
|
+
if (WRITE_REPORT) {
|
|
109
|
+
const lines = [];
|
|
110
|
+
lines.push('# KB Management Site Regression Test Report');
|
|
111
|
+
lines.push('');
|
|
112
|
+
lines.push(`**Generated**: ${new Date().toISOString()}`);
|
|
113
|
+
lines.push(`**Node**: ${process.version}`);
|
|
114
|
+
lines.push('**Runner**: node _site/_test/run-all-tests.js');
|
|
115
|
+
lines.push('');
|
|
116
|
+
lines.push('## Summary');
|
|
117
|
+
lines.push('');
|
|
118
|
+
lines.push(`**Per-test timeout**: ${PER_TEST_TIMEOUT_MS}ms (override with PK_TEST_TIMEOUT_MS)`);
|
|
119
|
+
lines.push('');
|
|
120
|
+
lines.push('| # | File | Status | Exit | Duration |');
|
|
121
|
+
lines.push('|--:|------|:------:|-----:|---------:|');
|
|
122
|
+
results.forEach((result, index) => {
|
|
123
|
+
const status = result.passed ? 'PASS' : (result.failureKind === 'timeout' ? 'TIMEOUT' : 'FAIL');
|
|
124
|
+
const exit = result.passed
|
|
125
|
+
? String(result.exitCode)
|
|
126
|
+
: `${result.exitCode}${result.signal ? `, signal ${result.signal}` : ''}${result.spawnErrorCode ? `, error ${result.spawnErrorCode}` : ''}`;
|
|
127
|
+
lines.push(`| ${index + 1} | \`${result.file}\` | ${status} | ${exit} | ${result.durationMs}ms |`);
|
|
128
|
+
});
|
|
129
|
+
lines.push('');
|
|
130
|
+
lines.push(`**Total**: ${results.length} | **Passed**: ${passed} | **Failed**: ${failed} | **Time**: ${totalDuration}ms`);
|
|
131
|
+
lines.push('');
|
|
132
|
+
|
|
133
|
+
const failures = results.filter(result => !result.passed);
|
|
134
|
+
if (failures.length) {
|
|
135
|
+
lines.push('## Failures');
|
|
136
|
+
lines.push('');
|
|
137
|
+
for (const result of failures) {
|
|
138
|
+
lines.push(`### ${result.file}`);
|
|
139
|
+
lines.push('');
|
|
140
|
+
lines.push('```');
|
|
141
|
+
lines.push(result.outputTail);
|
|
142
|
+
lines.push('```');
|
|
143
|
+
lines.push('');
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
try {
|
|
148
|
+
fs.writeFileSync(REPORT_PATH, lines.join('\n'), 'utf-8');
|
|
149
|
+
console.log(`Report written to: ${REPORT_PATH}`);
|
|
150
|
+
} catch (e) {
|
|
151
|
+
console.error(`Failed to write report: ${e.message}`);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
process.exit(failed > 0 ? 1 : 0);
|
|
156
|
+
})();
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
// Run: node _site/_test/runtime-endpoint-test.js
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const os = require('os');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
const runtime = require('../lib/runtime-endpoint');
|
|
7
|
+
|
|
8
|
+
function assert(condition, message) {
|
|
9
|
+
if (!condition) throw new Error(message);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const dataDir = fs.mkdtempSync(path.join(os.tmpdir(), `kb-runtime-endpoint-${process.pid}-`));
|
|
13
|
+
|
|
14
|
+
try {
|
|
15
|
+
assert(runtime.readEndpoint(dataDir) === null, 'missing endpoint should read as null');
|
|
16
|
+
|
|
17
|
+
const written = runtime.writeEndpoint(dataDir, {
|
|
18
|
+
pid: process.pid,
|
|
19
|
+
host: '127.0.0.1',
|
|
20
|
+
port: 5761,
|
|
21
|
+
mode: 'desktop',
|
|
22
|
+
});
|
|
23
|
+
assert(written.schema === runtime.SCHEMA, 'endpoint schema should be current');
|
|
24
|
+
assert(written.mode === 'desktop', 'endpoint should preserve runtime mode');
|
|
25
|
+
assert(runtime.readLiveEndpoint(dataDir).pid === process.pid, 'current process endpoint should be live');
|
|
26
|
+
|
|
27
|
+
runtime.clearEndpoint(dataDir, { pid: process.pid + 1 });
|
|
28
|
+
assert(runtime.readEndpoint(dataDir) !== null, 'a different process must not clear the endpoint');
|
|
29
|
+
runtime.clearEndpoint(dataDir, { pid: process.pid });
|
|
30
|
+
assert(runtime.readEndpoint(dataDir) === null, 'owner should clear the endpoint');
|
|
31
|
+
|
|
32
|
+
const claim = runtime.claimEndpoint(dataDir, {
|
|
33
|
+
pid: process.pid,
|
|
34
|
+
host: '127.0.0.1',
|
|
35
|
+
port: 5762,
|
|
36
|
+
mode: 'cli',
|
|
37
|
+
});
|
|
38
|
+
assert(claim.claimed === true, 'first process should claim an empty endpoint');
|
|
39
|
+
const duplicate = runtime.claimEndpoint(dataDir, {
|
|
40
|
+
pid: process.pid + 1,
|
|
41
|
+
host: '127.0.0.1',
|
|
42
|
+
port: 5763,
|
|
43
|
+
mode: 'desktop',
|
|
44
|
+
});
|
|
45
|
+
assert(duplicate.claimed === false, 'second process must not replace a live endpoint');
|
|
46
|
+
assert(duplicate.endpoint.pid === process.pid, 'duplicate should discover the owner');
|
|
47
|
+
runtime.clearEndpoint(dataDir, { pid: process.pid });
|
|
48
|
+
|
|
49
|
+
runtime.writeEndpoint(dataDir, { pid: 2147483646, host: 'localhost', port: 5757 });
|
|
50
|
+
assert(runtime.readLiveEndpoint(dataDir) === null, 'dead process endpoint should be rejected');
|
|
51
|
+
assert(!fs.existsSync(runtime.endpointPath(dataDir)), 'dead process endpoint should be removed');
|
|
52
|
+
|
|
53
|
+
fs.writeFileSync(runtime.endpointPath(dataDir), JSON.stringify({
|
|
54
|
+
schema: runtime.SCHEMA,
|
|
55
|
+
pid: process.pid,
|
|
56
|
+
host: 'example.com',
|
|
57
|
+
port: 5757,
|
|
58
|
+
}), 'utf-8');
|
|
59
|
+
assert(runtime.readEndpoint(dataDir) === null, 'remote hosts must never be accepted');
|
|
60
|
+
|
|
61
|
+
console.log('runtime-endpoint-test PASS');
|
|
62
|
+
} finally {
|
|
63
|
+
fs.rmSync(dataDir, { recursive: true, force: true });
|
|
64
|
+
}
|