@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,363 @@
|
|
|
1
|
+
// _site/_test/legacy-project-upgrade-e2e-test.js
|
|
2
|
+
//
|
|
3
|
+
// Characterization harness for the v4.1.22 -> main migration path.
|
|
4
|
+
// Builds a v4.1.22-shaped legacy data dir (projects.json, knowledge-store.json,
|
|
5
|
+
// ai-profiles.json) plus a v1 managed post-commit hook in a real git repo,
|
|
6
|
+
// then runs MigrationService + migrateManagedHook + CommitReconciler to prove
|
|
7
|
+
// the legacy project continues to work without manual re-import.
|
|
8
|
+
//
|
|
9
|
+
// These tests are designed to fail under the current regressions and pass
|
|
10
|
+
// after T01/T02/T03 fixes. They do NOT modify production code.
|
|
11
|
+
|
|
12
|
+
const assert = require('assert');
|
|
13
|
+
const fs = require('fs');
|
|
14
|
+
const os = require('os');
|
|
15
|
+
const path = require('path');
|
|
16
|
+
const { spawnSync } = require('child_process');
|
|
17
|
+
|
|
18
|
+
const { StorageLayout } = require('../lib/storage-layout');
|
|
19
|
+
const { MigrationService } = require('../lib/migration-service');
|
|
20
|
+
const {
|
|
21
|
+
installHook,
|
|
22
|
+
readHookStatus,
|
|
23
|
+
migrateManagedHook,
|
|
24
|
+
LEGACY_HOOK_MARKER,
|
|
25
|
+
HOOK_MARKER,
|
|
26
|
+
} = require('../lib/hook-manager');
|
|
27
|
+
const { ProjectStore } = require('../lib/project-store');
|
|
28
|
+
const { ProjectRegistryStore } = require('../lib/project-registry-store');
|
|
29
|
+
const { Logger } = require('../lib/structured-logger');
|
|
30
|
+
const { migrateManagedHooks } = require('../lib/server-app');
|
|
31
|
+
|
|
32
|
+
function git(repo, args) {
|
|
33
|
+
const result = spawnSync('git', ['-C', repo, ...args], {
|
|
34
|
+
encoding: 'utf8',
|
|
35
|
+
windowsHide: true,
|
|
36
|
+
env: { ...process.env, GIT_TERMINAL_PROMPT: '0' },
|
|
37
|
+
});
|
|
38
|
+
if (result.status !== 0) throw new Error(result.stderr || `git ${args.join(' ')} failed: ${result.status}`);
|
|
39
|
+
return String(result.stdout || '').trim();
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function gitOrNull(repo, args) {
|
|
43
|
+
const result = spawnSync('git', ['-C', repo, ...args], {
|
|
44
|
+
encoding: 'utf8',
|
|
45
|
+
windowsHide: true,
|
|
46
|
+
env: { ...process.env, GIT_TERMINAL_PROMPT: '0' },
|
|
47
|
+
});
|
|
48
|
+
return result.status === 0 ? String(result.stdout || '').trim() : null;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function makeRepo(repoPath) {
|
|
52
|
+
fs.mkdirSync(repoPath, { recursive: true });
|
|
53
|
+
git(repoPath, ['init', '-q', '-b', 'main']);
|
|
54
|
+
git(repoPath, ['config', 'user.email', 'legacy-e2e@example.local']);
|
|
55
|
+
git(repoPath, ['config', 'user.name', 'Legacy E2E']);
|
|
56
|
+
git(repoPath, ['config', 'commit.gpgsign', 'false']);
|
|
57
|
+
fs.writeFileSync(path.join(repoPath, 'README.md'), '# legacy\n');
|
|
58
|
+
git(repoPath, ['add', 'README.md']);
|
|
59
|
+
git(repoPath, ['commit', '-q', '-m', 'initial']);
|
|
60
|
+
return git(repoPath, ['rev-parse', 'HEAD']);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function makeLegacyFixture({ hookBody, includeHook = true } = {}) {
|
|
64
|
+
const dataDir = fs.mkdtempSync(path.join(os.tmpdir(), 'pk-legacy-upgrade-e2e-'));
|
|
65
|
+
const repo = path.join(dataDir, 'repos', 'legacy-sample');
|
|
66
|
+
const kbPath = path.join(dataDir, 'knowledge', 'legacy-sample');
|
|
67
|
+
const initialSha = makeRepo(repo);
|
|
68
|
+
if (includeHook) {
|
|
69
|
+
const gitDir = git(repo, ['rev-parse', '--path-format=absolute', '--git-dir']);
|
|
70
|
+
const hooksDir = path.join(path.resolve(gitDir), 'hooks');
|
|
71
|
+
fs.mkdirSync(hooksDir, { recursive: true });
|
|
72
|
+
fs.writeFileSync(path.join(hooksDir, 'post-commit'), hookBody || (
|
|
73
|
+
`#!/bin/sh\n${LEGACY_HOOK_MARKER} — auto-installed by KB manager (legacy)\n` +
|
|
74
|
+
`node 'C:/legacy/site/_site/scripts/hook-trigger.js' --kb-root 'C:/legacy/site' \\\n` +
|
|
75
|
+
` --repo "${repo.replace(/\\/g, '/')}" >/dev/null 2>&1 || true\nexit 0\n`
|
|
76
|
+
), { mode: 0o755 });
|
|
77
|
+
}
|
|
78
|
+
fs.mkdirSync(kbPath, { recursive: true });
|
|
79
|
+
fs.writeFileSync(path.join(kbPath, 'README.md'), '# legacy knowledge\n');
|
|
80
|
+
fs.writeFileSync(path.join(dataDir, 'projects.json'), JSON.stringify({
|
|
81
|
+
'legacy-sample': {
|
|
82
|
+
schema: 'kb-project/v1',
|
|
83
|
+
displayName: 'Legacy Sample',
|
|
84
|
+
localPath: repo,
|
|
85
|
+
gitPath: repo,
|
|
86
|
+
kbPath,
|
|
87
|
+
trackingStartCommit: initialSha,
|
|
88
|
+
lastAnalyzedCommit: initialSha,
|
|
89
|
+
enabled: true,
|
|
90
|
+
aiProfileId: null,
|
|
91
|
+
knowledgeLanguage: 'zh-CN',
|
|
92
|
+
},
|
|
93
|
+
}, null, 2));
|
|
94
|
+
fs.writeFileSync(path.join(dataDir, 'knowledge-store.json'), JSON.stringify({
|
|
95
|
+
schema: 'knowledge-store/v1',
|
|
96
|
+
rootPath: path.join(dataDir, 'knowledge'),
|
|
97
|
+
configured: true,
|
|
98
|
+
}));
|
|
99
|
+
fs.writeFileSync(path.join(dataDir, 'ai-profiles.json'), JSON.stringify({
|
|
100
|
+
schema: 'ai-profiles/v1',
|
|
101
|
+
profiles: [
|
|
102
|
+
{ id: 'primary', name: 'Primary', vendor: 'anthropic', model: 'claude-sonnet', enabled: true },
|
|
103
|
+
],
|
|
104
|
+
defaultProfileId: 'primary',
|
|
105
|
+
}));
|
|
106
|
+
return { dataDir, repo, kbPath, initialSha };
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
async function run() {
|
|
110
|
+
const ROOT = path.resolve(__dirname, '..', '..');
|
|
111
|
+
const TRIGGER = path.join(ROOT, '_site', 'scripts', 'hook-trigger.js');
|
|
112
|
+
|
|
113
|
+
// ---- Case 1: legacy v1 hook must be upgraded by migrateManagedHook ----
|
|
114
|
+
{
|
|
115
|
+
const { dataDir, repo, kbPath, initialSha } = makeLegacyFixture();
|
|
116
|
+
const layout = new StorageLayout({ dataDir });
|
|
117
|
+
const migration = new MigrationService({ layout, legacyDataDir: dataDir });
|
|
118
|
+
const result = await migration.migrateIfNeeded({ migrationRunId: 'legacy-upgrade-v1' });
|
|
119
|
+
assert.strictEqual(result.ok, true, 'migration must succeed for a legacy fixture with v1 hook');
|
|
120
|
+
assert.strictEqual(result.completed, true, 'migration must complete');
|
|
121
|
+
const projectId = result.projectMap['legacy-sample'];
|
|
122
|
+
assert(projectId, 'migration must produce deterministic projectId mapping');
|
|
123
|
+
// Repo path preserved (not replaced with data-dir fallback)
|
|
124
|
+
const config = JSON.parse(fs.readFileSync(layout.getProjectConfigPath(projectId), 'utf8'));
|
|
125
|
+
assert.strictEqual(config.repoPath, path.resolve(repo), 'migrated repoPath must point at the real legacy repo, not a data-dir fallback');
|
|
126
|
+
assert.strictEqual(config.knowledgePath, path.resolve(kbPath), 'migrated knowledgePath must equal legacy kbPath');
|
|
127
|
+
assert.strictEqual(config.knowledgeLanguage, 'zh-CN', 'migrated knowledgeLanguage must be preserved');
|
|
128
|
+
const state = JSON.parse(fs.readFileSync(layout.getProjectStatePath(projectId), 'utf8'));
|
|
129
|
+
assert.strictEqual(state.trackingStartCommit, initialSha, 'trackingStartCommit must be preserved');
|
|
130
|
+
assert.strictEqual(state.lastAnalyzedCommit, initialSha, 'lastAnalyzedCommit must be preserved');
|
|
131
|
+
// Now invoke migrateManagedHook against the migrated repo: legacy v1 -> v2
|
|
132
|
+
const migrated = migrateManagedHook({ repoPath: repo, projectId, triggerScriptPath: TRIGGER });
|
|
133
|
+
assert.strictEqual(migrated.migrated, true, 'migrateManagedHook must report legacy->v2 migration');
|
|
134
|
+
assert.strictEqual(migrated.managedVersion, 2, 'managedVersion must advance to 2');
|
|
135
|
+
const status = readHookStatus({ repoPath: repo, projectId });
|
|
136
|
+
assert.strictEqual(status.kbManaged, true, 'after migration hook must be kb-managed');
|
|
137
|
+
assert.strictEqual(status.managedVersion, 2, 'after migration hook must be v2');
|
|
138
|
+
const hookBody = fs.readFileSync(status.hookPath, 'utf8');
|
|
139
|
+
assert(hookBody.includes(HOOK_MARKER), 'migrated hook body must carry the v2 marker');
|
|
140
|
+
assert(!hookBody.includes(LEGACY_HOOK_MARKER), 'migrated hook body must NOT carry the legacy v1 marker');
|
|
141
|
+
fs.rmSync(dataDir, { recursive: true, force: true });
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// ---- Case 2: idempotent migration (second run) ----
|
|
145
|
+
{
|
|
146
|
+
const { dataDir } = makeLegacyFixture();
|
|
147
|
+
const layout = new StorageLayout({ dataDir });
|
|
148
|
+
const migration = new MigrationService({ layout, legacyDataDir: dataDir });
|
|
149
|
+
const first = await migration.migrateIfNeeded({ migrationRunId: 'legacy-idempotent' });
|
|
150
|
+
assert.strictEqual(first.completed, true);
|
|
151
|
+
const second = await migration.migrateIfNeeded();
|
|
152
|
+
assert.strictEqual(second.completed, true);
|
|
153
|
+
assert.strictEqual(second.migrated, false, 'second migration must be idempotent (no re-migration)');
|
|
154
|
+
fs.rmSync(dataDir, { recursive: true, force: true });
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// ---- Case 3: missing legacy hook -> install + verify -> mark migrated ----
|
|
158
|
+
// T03 state-machine case 3: a missing hook must be installed and verified
|
|
159
|
+
// before migrationVersion=2 is written. The pre-fix bug (RC-02) was that
|
|
160
|
+
// migrateManagedHooks unconditionally wrote migrationVersion=2 even when
|
|
161
|
+
// migrateManagedHook returned { reason: 'missing' } with no install.
|
|
162
|
+
{
|
|
163
|
+
const { dataDir, repo } = makeLegacyFixture({ includeHook: false });
|
|
164
|
+
const layout = new StorageLayout({ dataDir });
|
|
165
|
+
const migration = new MigrationService({ layout, legacyDataDir: dataDir });
|
|
166
|
+
const result = await migration.migrateIfNeeded({ migrationRunId: 'legacy-missing-hook' });
|
|
167
|
+
assert.strictEqual(result.ok, true);
|
|
168
|
+
const projectId = result.projectMap['legacy-sample'];
|
|
169
|
+
const preStatus = readHookStatus({ repoPath: repo, projectId });
|
|
170
|
+
assert.strictEqual(preStatus.installed, false, 'fixture must not include a hook file');
|
|
171
|
+
const projectStore = new ProjectStore({ layout });
|
|
172
|
+
const registryStore = new ProjectRegistryStore({ layout });
|
|
173
|
+
await registryStore.initialize();
|
|
174
|
+
const logger = new Logger({ layout, settingsProvider: () => ({ levels: [] }), context: { component: 'legacy-upgrade-e2e' } });
|
|
175
|
+
const runtime = { registryStore, projectStore, logger, layout };
|
|
176
|
+
const hooks = await migrateManagedHooks(runtime);
|
|
177
|
+
const entry = hooks.find(item => item.projectId === projectId);
|
|
178
|
+
assert(entry, 'migrateManagedHooks must produce an entry for the migrated legacy project');
|
|
179
|
+
assert.strictEqual(entry.status, 'verified', 'missing hook must be installed+verified, not silently marked migrated');
|
|
180
|
+
assert.strictEqual(entry.reason, 'installed', 'a previously-missing hook reports reason=installed after migration');
|
|
181
|
+
const postStatus = readHookStatus({ repoPath: repo, projectId });
|
|
182
|
+
assert.strictEqual(postStatus.kbManaged, true, 'after migration the hook must be kb-managed');
|
|
183
|
+
assert.strictEqual(postStatus.managedVersion, 2, 'after migration the hook must be v2');
|
|
184
|
+
const finalState = projectStore.readState(projectId);
|
|
185
|
+
assert.strictEqual(finalState.hook.migrationVersion, 2, 'after successful install+verify, migrationVersion must advance to 2');
|
|
186
|
+
assert(finalState.hook.lastVerifiedAt && /^\d{4}-/.test(finalState.hook.lastVerifiedAt), 'after migration lastVerifiedAt must be a timestamp');
|
|
187
|
+
await logger.close();
|
|
188
|
+
fs.rmSync(dataDir, { recursive: true, force: true });
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// ---- Case 3b: missing hook + install failure -> migrationVersion stays < 2 ----
|
|
192
|
+
// When the install step itself fails (e.g. the repo path is gone), the
|
|
193
|
+
// state machine must leave migrationVersion unchanged so the next startup
|
|
194
|
+
// can retry. The pre-fix bug advanced migrationVersion even when no hook
|
|
195
|
+
// was installed at all.
|
|
196
|
+
{
|
|
197
|
+
const { dataDir, repo } = makeLegacyFixture({ includeHook: false });
|
|
198
|
+
const layout = new StorageLayout({ dataDir });
|
|
199
|
+
const migration = new MigrationService({ layout, legacyDataDir: dataDir });
|
|
200
|
+
const result = await migration.migrateIfNeeded({ migrationRunId: 'legacy-missing-install-fail' });
|
|
201
|
+
assert.strictEqual(result.ok, true);
|
|
202
|
+
const projectId = result.projectMap['legacy-sample'];
|
|
203
|
+
// Remove the repo so installHook will fail (missing repo).
|
|
204
|
+
fs.rmSync(repo, { recursive: true, force: true });
|
|
205
|
+
const projectStore = new ProjectStore({ layout });
|
|
206
|
+
const registryStore = new ProjectRegistryStore({ layout });
|
|
207
|
+
await registryStore.initialize();
|
|
208
|
+
const logger = new Logger({ layout, settingsProvider: () => ({ levels: [] }), context: { component: 'legacy-upgrade-e2e' } });
|
|
209
|
+
const runtime = { registryStore, projectStore, logger, layout };
|
|
210
|
+
const hooks = await migrateManagedHooks(runtime);
|
|
211
|
+
// migrateManagedHooks skips projects whose repoPath is missing (the
|
|
212
|
+
// fs.existsSync check at the top of the loop). So the registry entry
|
|
213
|
+
// should not appear here. That is itself the correct behavior — we
|
|
214
|
+
// only assert it does NOT silently mark migrationVersion=2 when the
|
|
215
|
+
// repo is gone.
|
|
216
|
+
const entry = hooks.find(item => item.projectId === projectId);
|
|
217
|
+
if (entry) {
|
|
218
|
+
assert.notStrictEqual(entry.status, 'verified', 'a project whose repo is missing must not be marked verified');
|
|
219
|
+
}
|
|
220
|
+
const finalState = projectStore.readState(projectId);
|
|
221
|
+
assert.strictEqual(finalState.hook.migrationVersion < 2, true,
|
|
222
|
+
'RC-02 regression: a missing-repo project must not be marked migrationVersion=2');
|
|
223
|
+
await logger.close();
|
|
224
|
+
fs.rmSync(dataDir, { recursive: true, force: true });
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// ---- Case 4: CHARACTERIZATION — third-party hook must not be silently overwritten ----
|
|
228
|
+
{
|
|
229
|
+
const { dataDir, repo } = makeLegacyFixture();
|
|
230
|
+
const gitDir = git(repo, ['rev-parse', '--path-format=absolute', '--git-dir']);
|
|
231
|
+
const hooksDir = path.join(path.resolve(gitDir), 'hooks');
|
|
232
|
+
fs.mkdirSync(hooksDir, { recursive: true });
|
|
233
|
+
const userHook = '#!/bin/sh\necho "user owned hook"\nexit 0\n';
|
|
234
|
+
fs.writeFileSync(path.join(hooksDir, 'post-commit'), userHook, { mode: 0o755 });
|
|
235
|
+
const layout = new StorageLayout({ dataDir });
|
|
236
|
+
const migration = new MigrationService({ layout, legacyDataDir: dataDir });
|
|
237
|
+
const result = await migration.migrateIfNeeded({ migrationRunId: 'legacy-third-party' });
|
|
238
|
+
assert.strictEqual(result.ok, true);
|
|
239
|
+
const projectId = result.projectMap['legacy-sample'];
|
|
240
|
+
let threw = false;
|
|
241
|
+
try {
|
|
242
|
+
migrateManagedHook({ repoPath: repo, projectId, triggerScriptPath: TRIGGER });
|
|
243
|
+
} catch (error) {
|
|
244
|
+
threw = error && (error.code === 'HOOK_CONFLICT' || /third-party|cannot be migrated/i.test(String(error.message || '')));
|
|
245
|
+
}
|
|
246
|
+
assert.strictEqual(threw, true, 'migrateManagedHook must reject a third-party post-commit hook');
|
|
247
|
+
const after = fs.readFileSync(path.join(hooksDir, 'post-commit'), 'utf8');
|
|
248
|
+
assert.strictEqual(after, userHook, 'third-party hook body must be untouched');
|
|
249
|
+
fs.rmSync(dataDir, { recursive: true, force: true });
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// ---- Case 5: end-to-end commit on a migrated legacy project ----
|
|
253
|
+
// Validates that legacy knowledgePath/tracking survive a real commit cycle without manual re-import.
|
|
254
|
+
{
|
|
255
|
+
const { dataDir, repo, initialSha } = makeLegacyFixture();
|
|
256
|
+
const layout = new StorageLayout({ dataDir });
|
|
257
|
+
const migration = new MigrationService({ layout, legacyDataDir: dataDir });
|
|
258
|
+
const result = await migration.migrateIfNeeded({ migrationRunId: 'legacy-commit' });
|
|
259
|
+
assert.strictEqual(result.ok, true);
|
|
260
|
+
const projectId = result.projectMap['legacy-sample'];
|
|
261
|
+
// The fixture includes a v1 hook, so use migrateManagedHook which replaces
|
|
262
|
+
// legacy v1 with v2 using the T02 runtime contract.
|
|
263
|
+
const migrated = migrateManagedHook({ repoPath: repo, projectId, triggerScriptPath: TRIGGER });
|
|
264
|
+
assert.strictEqual(migrated.migrated, true, 'migrateManagedHook must report v1 -> v2');
|
|
265
|
+
assert.strictEqual(migrated.managedVersion, 2, 'migrated managedVersion must be 2');
|
|
266
|
+
// New commit in the legacy repo
|
|
267
|
+
fs.writeFileSync(path.join(repo, 'change.txt'), 'second\n');
|
|
268
|
+
git(repo, ['add', 'change.txt']);
|
|
269
|
+
git(repo, ['commit', '-q', '-m', 'second']);
|
|
270
|
+
const newHead = git(repo, ['rev-parse', 'HEAD']);
|
|
271
|
+
assert.notStrictEqual(newHead, initialSha, 'fixture must create a second commit');
|
|
272
|
+
// Verify hook is present and v2
|
|
273
|
+
const status = readHookStatus({ repoPath: repo, projectId });
|
|
274
|
+
assert.strictEqual(status.kbManaged, true);
|
|
275
|
+
assert.strictEqual(status.managedVersion, 2);
|
|
276
|
+
fs.rmSync(dataDir, { recursive: true, force: true });
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// ---- Case 6: second startup after successful migration -> no duplicate rewrite ----
|
|
280
|
+
// The post-T03 contract: once a project is verified migrated, a subsequent
|
|
281
|
+
// startup must not touch the hook file or rewrite migrationVersion.
|
|
282
|
+
{
|
|
283
|
+
const { dataDir, repo } = makeLegacyFixture({ includeHook: false });
|
|
284
|
+
const layout = new StorageLayout({ dataDir });
|
|
285
|
+
const migration = new MigrationService({ layout, legacyDataDir: dataDir });
|
|
286
|
+
const result = await migration.migrateIfNeeded({ migrationRunId: 'legacy-second-startup' });
|
|
287
|
+
assert.strictEqual(result.ok, true);
|
|
288
|
+
const projectId = result.projectMap['legacy-sample'];
|
|
289
|
+
const projectStore = new ProjectStore({ layout });
|
|
290
|
+
const registryStore = new ProjectRegistryStore({ layout });
|
|
291
|
+
await registryStore.initialize();
|
|
292
|
+
const logger = new Logger({ layout, settingsProvider: () => ({ levels: [] }), context: { component: 'legacy-upgrade-e2e' } });
|
|
293
|
+
const runtime = { registryStore, projectStore, logger, layout };
|
|
294
|
+
// First startup
|
|
295
|
+
const firstPass = await migrateManagedHooks(runtime);
|
|
296
|
+
assert(firstPass.length > 0, `migrateManagedHooks must produce at least one entry; got ${JSON.stringify(firstPass)}`);
|
|
297
|
+
const firstEntry = firstPass.find(item => item.projectId === projectId);
|
|
298
|
+
assert(firstEntry, `first pass must include the migrated project; got: ${JSON.stringify(firstPass.map(p => p.projectId))}`);
|
|
299
|
+
assert.strictEqual(firstEntry.status, 'verified');
|
|
300
|
+
assert.strictEqual(firstEntry.reason, 'installed');
|
|
301
|
+
const hookPath = readHookStatus({ repoPath: repo, projectId }).hookPath;
|
|
302
|
+
const firstMtime = fs.statSync(hookPath).mtimeMs;
|
|
303
|
+
const firstBody = fs.readFileSync(hookPath, 'utf8');
|
|
304
|
+
const firstState = projectStore.readState(projectId);
|
|
305
|
+
const firstVerifiedAt = firstState.hook.lastVerifiedAt;
|
|
306
|
+
// Wait briefly so a rewrite would change mtime
|
|
307
|
+
await new Promise(resolve => setTimeout(resolve, 20));
|
|
308
|
+
// Second startup
|
|
309
|
+
const secondPass = await migrateManagedHooks(runtime);
|
|
310
|
+
const secondEntry = secondPass.find(item => item.projectId === projectId);
|
|
311
|
+
assert.strictEqual(secondEntry.status, 'verified');
|
|
312
|
+
assert.strictEqual(secondEntry.reason, 'already-migrated', 'second startup must report reason=already-migrated without rewriting');
|
|
313
|
+
assert.strictEqual(fs.readFileSync(hookPath, 'utf8'), firstBody, 'hook body must be untouched on second startup');
|
|
314
|
+
assert.strictEqual(fs.statSync(hookPath).mtimeMs, firstMtime, 'hook mtime must be unchanged on second startup');
|
|
315
|
+
const secondState = projectStore.readState(projectId);
|
|
316
|
+
assert.strictEqual(secondState.hook.lastVerifiedAt, firstVerifiedAt, 'lastVerifiedAt must not be re-stamped on idempotent pass');
|
|
317
|
+
await logger.close();
|
|
318
|
+
fs.rmSync(dataDir, { recursive: true, force: true });
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// ---- Case 7: third-party hook -> migration incomplete (T03 case 4) ----
|
|
322
|
+
// Verifies the migrateManagedHooks state machine reports conflict without
|
|
323
|
+
// overwriting the user's hook.
|
|
324
|
+
{
|
|
325
|
+
const { dataDir, repo } = makeLegacyFixture({ includeHook: false });
|
|
326
|
+
const gitDir = git(repo, ['rev-parse', '--path-format=absolute', '--git-dir']);
|
|
327
|
+
const hooksDir = path.join(path.resolve(gitDir), 'hooks');
|
|
328
|
+
fs.mkdirSync(hooksDir, { recursive: true });
|
|
329
|
+
const userHook = '#!/bin/sh\necho "third-party"\nexit 0\n';
|
|
330
|
+
fs.writeFileSync(path.join(hooksDir, 'post-commit'), userHook, { mode: 0o755 });
|
|
331
|
+
const layout = new StorageLayout({ dataDir });
|
|
332
|
+
const migration = new MigrationService({ layout, legacyDataDir: dataDir });
|
|
333
|
+
const result = await migration.migrateIfNeeded({ migrationRunId: 'legacy-third-party-state' });
|
|
334
|
+
assert.strictEqual(result.ok, true);
|
|
335
|
+
const projectId = result.projectMap['legacy-sample'];
|
|
336
|
+
const projectStore = new ProjectStore({ layout });
|
|
337
|
+
const registryStore = new ProjectRegistryStore({ layout });
|
|
338
|
+
await registryStore.initialize();
|
|
339
|
+
const logger = new Logger({ layout, settingsProvider: () => ({ levels: [] }), context: { component: 'legacy-upgrade-e2e' } });
|
|
340
|
+
const runtime = { registryStore, projectStore, logger, layout };
|
|
341
|
+
const pass = await migrateManagedHooks(runtime);
|
|
342
|
+
const entry = pass.find(item => item.projectId === projectId);
|
|
343
|
+
assert(entry, 'migrateManagedHooks must produce an entry for the third-party-hook project');
|
|
344
|
+
assert.strictEqual(entry.status, 'conflict', 'third-party hook must be reported as conflict, not verified');
|
|
345
|
+
assert.strictEqual(entry.reason, 'third-party');
|
|
346
|
+
const finalState = projectStore.readState(projectId);
|
|
347
|
+
assert.strictEqual(finalState.hook.migrationVersion < 2, true,
|
|
348
|
+
'third-party hook must not be marked migrationVersion=2');
|
|
349
|
+
assert(finalState.hook.lastConflict && finalState.hook.lastConflict.code === 'HOOK_CONFLICT',
|
|
350
|
+
'third-party conflict must be persisted in lastConflict for UI consumption');
|
|
351
|
+
assert.strictEqual(fs.readFileSync(path.join(hooksDir, 'post-commit'), 'utf8'), userHook,
|
|
352
|
+
'third-party hook body must remain untouched');
|
|
353
|
+
await logger.close();
|
|
354
|
+
fs.rmSync(dataDir, { recursive: true, force: true });
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
console.log('legacy-project-upgrade-e2e-test PASS');
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
run().catch(error => {
|
|
361
|
+
console.error(error.stack || error.message);
|
|
362
|
+
process.exit(1);
|
|
363
|
+
});
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
const assert = require('assert');
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const os = require('os');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const { spawnServer } = require('./helpers/spawn-server');
|
|
6
|
+
|
|
7
|
+
const ROOT = path.resolve(__dirname, '..', '..');
|
|
8
|
+
const PORT = 7833;
|
|
9
|
+
const BASE = `http://127.0.0.1:${PORT}`;
|
|
10
|
+
const dataDir = fs.mkdtempSync(path.join(os.tmpdir(), 'pkb-routes-v2-'));
|
|
11
|
+
|
|
12
|
+
async function waitForServer() {
|
|
13
|
+
for (let attempt = 0; attempt < 100; attempt += 1) {
|
|
14
|
+
try { if ((await fetch(`${BASE}/api/health`)).ok) return; } catch {}
|
|
15
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
16
|
+
}
|
|
17
|
+
throw new Error('server did not start');
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
(async () => {
|
|
21
|
+
const source = `${fs.readFileSync(path.join(ROOT, '_site', 'server.js'), 'utf8')}\n${fs.readFileSync(path.join(ROOT, '_site', 'lib', 'server-app.js'), 'utf8')}`;
|
|
22
|
+
for (const removedSymbol of ['automation/simulate', 'automation/init', 'hook-install', 'hook-uninstall', 'dispatchProjectInit', 'DEFAULT_INIT_PROMPT_TEMPLATE', 'renderProjectInitPrompt', '/api/raw']) {
|
|
23
|
+
assert(!source.includes(removedSymbol), `removed production symbol remains: ${removedSymbol}`);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const spawned = spawnServer({ root: ROOT, port: PORT, dataDir, tag: 'routes-v2', extraEnv: { KB_SKIP_MIGRATION: '1' } });
|
|
27
|
+
try {
|
|
28
|
+
await waitForServer();
|
|
29
|
+
const routes = [
|
|
30
|
+
['PUT', '/api/projects'],
|
|
31
|
+
['POST', '/api/projects/project-example/init'],
|
|
32
|
+
['POST', '/api/projects/project-example/automation/simulate'],
|
|
33
|
+
['POST', '/api/projects/project-example/automation/init'],
|
|
34
|
+
['GET', '/api/projects/project-example/hook-status'],
|
|
35
|
+
['POST', '/api/projects/project-example/hook-install'],
|
|
36
|
+
['POST', '/api/projects/project-example/hook-uninstall'],
|
|
37
|
+
['GET', '/api/raw?path=README.md'],
|
|
38
|
+
];
|
|
39
|
+
for (const [method, pathname] of routes) {
|
|
40
|
+
const response = await fetch(`${BASE}${pathname}`, { method });
|
|
41
|
+
assert.strictEqual(response.status, 404, `${method} ${pathname} should be removed`);
|
|
42
|
+
}
|
|
43
|
+
console.log('legacy-routes-removed-test PASS');
|
|
44
|
+
} finally {
|
|
45
|
+
await spawned.cleanup();
|
|
46
|
+
}
|
|
47
|
+
})().catch(error => { console.error(error); process.exitCode = 1; });
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
// Run: node _site/_test/legacy-workbench-exclusion-test.js
|
|
2
|
+
//
|
|
3
|
+
// T17 / GATE LEGACY-001: old embedded Workbench pairs are excluded from
|
|
4
|
+
// Development Conversation presentation and new commit binding; genuine
|
|
5
|
+
// explicit MCP requirements survive; immutable snapshots and JSONL lines are
|
|
6
|
+
// never rewritten.
|
|
7
|
+
|
|
8
|
+
const assert = require('assert');
|
|
9
|
+
const fs = require('fs');
|
|
10
|
+
const os = require('os');
|
|
11
|
+
const path = require('path');
|
|
12
|
+
const { StorageLayout } = require('../lib/storage-layout');
|
|
13
|
+
const { ProjectStore } = require('../lib/project-store');
|
|
14
|
+
const { ConversationStore } = require('../lib/conversation-store');
|
|
15
|
+
const { CommitConversationBinder } = require('../lib/commit-conversation-binder');
|
|
16
|
+
const { computeConversationExclusions, readDevelopmentEvents } = require('../lib/conversation-exclusions');
|
|
17
|
+
|
|
18
|
+
const temp = fs.mkdtempSync(path.join(os.tmpdir(), `kb-legacy-exclusion-${process.pid}-`));
|
|
19
|
+
const layout = new StorageLayout({ dataDir: path.join(temp, 'data') });
|
|
20
|
+
const projects = new ProjectStore({ layout });
|
|
21
|
+
const store = new ConversationStore({ layout, projectStore: projects, logger: null });
|
|
22
|
+
const projectId = 'project-legacy';
|
|
23
|
+
|
|
24
|
+
const repoIdentity = { commonDir: path.join(temp, 'repo', '.git') };
|
|
25
|
+
|
|
26
|
+
async function append(input) {
|
|
27
|
+
await store.appendEvent(projectId, {
|
|
28
|
+
source: 'claude-code',
|
|
29
|
+
sessionId: 'legacy-session',
|
|
30
|
+
projectPath: path.join(temp, 'repo'),
|
|
31
|
+
repoIdentity,
|
|
32
|
+
branch: 'main',
|
|
33
|
+
capturedAt: '2026-08-01T00:00:00.000Z',
|
|
34
|
+
...input,
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
(async () => {
|
|
39
|
+
await projects.create(projectId, {
|
|
40
|
+
displayName: 'Legacy', storageName: 'legacy',
|
|
41
|
+
repoPath: path.join(temp, 'repo'), knowledgePath: path.join(temp, 'knowledge'),
|
|
42
|
+
repoIdentity,
|
|
43
|
+
}, { conversationBaselineCursor: 0 });
|
|
44
|
+
|
|
45
|
+
// Old embedded Workbench pair (explicit-REQ1 + embedded-assistant-REQ1).
|
|
46
|
+
await append({ eventId: 'explicit-REQ1', sequence: 1, eventType: 'user_prompt', role: 'user', content: 'Workbench prompt', turnId: 'turn-embed-1', identityConfidence: 'high' });
|
|
47
|
+
await append({ eventId: 'embedded-assistant-REQ1', sequence: 2, eventType: 'assistant_response', role: 'assistant', content: 'Workbench reply', turnId: 'turn-embed-1', rawEventType: 'embedded-claude-result', identityConfidence: 'high' });
|
|
48
|
+
// Genuine explicit MCP requirement NOT paired with an embedded assistant.
|
|
49
|
+
await append({ eventId: 'explicit-REQ2', sequence: 3, eventType: 'user_prompt', role: 'user', content: 'Genuine MCP requirement', turnId: 'turn-explicit-2', identityConfidence: 'explicit', explicitCommitSha: 'f'.repeat(40) });
|
|
50
|
+
// Modern bridge-projected event.
|
|
51
|
+
await append({ eventId: 'evt-bridge-1', sequence: 4, eventType: 'user_prompt', role: 'user', content: 'Modern bridge event', turnId: 'turn-bridge', identityConfidence: 'exact' });
|
|
52
|
+
|
|
53
|
+
const eventsRaw = store.readEvents(projectId);
|
|
54
|
+
assert.strictEqual(eventsRaw.length, 4, 'raw JSONL lines are never deleted');
|
|
55
|
+
|
|
56
|
+
const manifest = computeConversationExclusions(projectId, eventsRaw);
|
|
57
|
+
assert.deepStrictEqual(manifest.excludedEventIds, ['embedded-assistant-REQ1', 'explicit-REQ1'], 'only the embedded pair is excluded');
|
|
58
|
+
|
|
59
|
+
const development = readDevelopmentEvents(store, projectId);
|
|
60
|
+
assert.deepStrictEqual(development.map(event => event.eventId), ['explicit-REQ2', 'evt-bridge-1'], 'genuine explicit + bridge events survive');
|
|
61
|
+
assert.strictEqual(development.some(event => event.content === 'Workbench prompt'), false);
|
|
62
|
+
assert.strictEqual(development.some(event => event.content === 'Workbench reply'), false);
|
|
63
|
+
|
|
64
|
+
// Manifest persisted for audit; JSONL untouched.
|
|
65
|
+
const manifestFile = path.join(layout.getProjectMetadataDir(projectId), 'conversation-exclusions.json');
|
|
66
|
+
assert.ok(fs.existsSync(manifestFile), 'exclusion manifest is persisted');
|
|
67
|
+
assert.strictEqual(JSON.parse(fs.readFileSync(manifestFile, 'utf8')).schema, 'conversation-exclusions/v1');
|
|
68
|
+
assert.strictEqual(store.readEvents(projectId).length, 4, 'raw store still holds every historical line');
|
|
69
|
+
|
|
70
|
+
// New commit binding excludes the legacy pair.
|
|
71
|
+
const binder = new CommitConversationBinder({ layout, projectStore: projects, conversationStore: store });
|
|
72
|
+
const commitSha = 'a'.repeat(40);
|
|
73
|
+
store.writeBoundary(projectId, {
|
|
74
|
+
repoIdentity, commitSha, parentShas: [], branch: 'main', committedAt: '2026-08-02T00:00:00.000Z',
|
|
75
|
+
bridgeCursorAtCommit: 10, journalSequence: 10, openTurnIdsAtCommit: [], operationId: 'op-legacy',
|
|
76
|
+
});
|
|
77
|
+
const snapshot = await binder.bind({ projectId, commitSha });
|
|
78
|
+
assert.ok(!JSON.stringify(snapshot).includes('Workbench prompt'), 'legacy embedded pair never binds to a new commit');
|
|
79
|
+
assert.ok(!JSON.stringify(snapshot).includes('embedded-claude-result'));
|
|
80
|
+
|
|
81
|
+
// An old frozen snapshot written before the migration stays byte-identical.
|
|
82
|
+
const frozenPath = layout.getProjectCommitConversationPath(projectId, commitSha);
|
|
83
|
+
const before = fs.readFileSync(frozenPath, 'utf8');
|
|
84
|
+
const rebound = await binder.bind({ projectId, commitSha });
|
|
85
|
+
assert.strictEqual(rebound.snapshotHash, snapshot.snapshotHash, 'immutable snapshot untouched by exclusion');
|
|
86
|
+
assert.strictEqual(fs.readFileSync(frozenPath, 'utf8'), before);
|
|
87
|
+
|
|
88
|
+
console.log('legacy-workbench-exclusion-test PASS');
|
|
89
|
+
})().catch(error => {
|
|
90
|
+
console.error(error.stack || error.message);
|
|
91
|
+
process.exitCode = 1;
|
|
92
|
+
});
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
const assert = require('assert');
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const os = require('os');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const { StorageLayout } = require('../lib/storage-layout');
|
|
6
|
+
const { Logger, LogRepository, redactValue, serializeError } = require('../lib/structured-logger');
|
|
7
|
+
|
|
8
|
+
(async () => {
|
|
9
|
+
const secret = 'sk-secret-123456';
|
|
10
|
+
const value = redactValue({
|
|
11
|
+
apiKey: secret,
|
|
12
|
+
headers: { authorization: 'Bearer ' + secret },
|
|
13
|
+
url: 'https://user:' + secret + '@example.invalid/path?token=' + secret,
|
|
14
|
+
nested: [{ clientSecret: secret }, { safe: 'visible' }],
|
|
15
|
+
}, { secrets: [secret] });
|
|
16
|
+
const text = JSON.stringify(value);
|
|
17
|
+
assert(!text.includes(secret));
|
|
18
|
+
assert(text.includes('visible'));
|
|
19
|
+
|
|
20
|
+
const cause = Object.assign(new Error('cause ' + secret), { code: 'ECAUSE' });
|
|
21
|
+
const error = Object.assign(new Error('Bearer ' + secret), { code: 'ETEST', cause });
|
|
22
|
+
const serialized = JSON.stringify(serializeError(error, { secrets: [secret] }));
|
|
23
|
+
assert(!serialized.includes(secret));
|
|
24
|
+
assert(serialized.includes('ETEST'));
|
|
25
|
+
assert(serialized.includes('ECAUSE'));
|
|
26
|
+
|
|
27
|
+
const dataDir = fs.mkdtempSync(path.join(os.tmpdir(), 'pk-dynamic-redaction-'));
|
|
28
|
+
try {
|
|
29
|
+
const layout = new StorageLayout({ dataDir });
|
|
30
|
+
let currentSecret = 'dynamic-secret-one';
|
|
31
|
+
const settings = { levels: ['info', 'error'], retentionDays: 365, maxTotalSizeMB: 10 };
|
|
32
|
+
const secretsProvider = () => [currentSecret];
|
|
33
|
+
const logger = new Logger({ layout, settingsProvider: () => settings, secretsProvider });
|
|
34
|
+
await logger.info('secret.first', 'first ' + currentSecret, { context: { echoed: currentSecret } });
|
|
35
|
+
currentSecret = 'dynamic-secret-two';
|
|
36
|
+
await logger.error('secret.second', 'second ' + currentSecret, { error: new Error('failure ' + currentSecret) });
|
|
37
|
+
await logger.close();
|
|
38
|
+
|
|
39
|
+
const repository = new LogRepository({ layout, settingsProvider: () => settings, secretsProvider });
|
|
40
|
+
const page = repository.query({ from: '2000-01-01', to: '2999-12-31' });
|
|
41
|
+
const queried = JSON.stringify(page);
|
|
42
|
+
assert(!queried.includes(currentSecret), 'current configured secret must be redacted during query');
|
|
43
|
+
assert(!queried.includes('dynamic-secret-one'), 'write-time configured secret must be redacted before persistence');
|
|
44
|
+
|
|
45
|
+
const exportPath = path.join(dataDir, 'export.jsonl');
|
|
46
|
+
repository.exportToFile(exportPath, { from: '2000-01-01', to: '2999-12-31' });
|
|
47
|
+
const exported = fs.readFileSync(exportPath, 'utf8');
|
|
48
|
+
assert(!exported.includes('dynamic-secret-one'));
|
|
49
|
+
assert(!exported.includes('dynamic-secret-two'));
|
|
50
|
+
} finally {
|
|
51
|
+
fs.rmSync(dataDir, { recursive: true, force: true });
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
console.log('log-redaction-test PASS');
|
|
55
|
+
})().catch(error => {
|
|
56
|
+
console.error(error.stack || error.message);
|
|
57
|
+
process.exitCode = 1;
|
|
58
|
+
});
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
const assert = require('assert');
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const os = require('os');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const { StorageLayout } = require('../lib/storage-layout');
|
|
6
|
+
const { Logger, LogRepository, localDay, normalizeRecord } = require('../lib/structured-logger');
|
|
7
|
+
|
|
8
|
+
function line(entry) {
|
|
9
|
+
return `${JSON.stringify(normalizeRecord(entry))}\r\n`;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
(async () => {
|
|
13
|
+
const dataDir = fs.mkdtempSync(path.join(os.tmpdir(), 'pk-log-repository-'));
|
|
14
|
+
const layout = new StorageLayout({ dataDir });
|
|
15
|
+
const settings = { levels: ['trace', 'debug', 'info', 'warn', 'error', 'fatal'] };
|
|
16
|
+
const logger = new Logger({ layout, settingsProvider: () => settings });
|
|
17
|
+
await logger.info('app.started', 'Started', { component: 'server', operationId: 'op-a' });
|
|
18
|
+
await logger.info('app.completed', 'Completed', { component: 'server', operationId: 'op-a', durationMs: 12 });
|
|
19
|
+
await logger.warn('project.deleted', 'Deleted project snapshot', { component: 'lifecycle', projectId: 'project-a', projectDisplayName: 'A', projectDeleted: true });
|
|
20
|
+
await logger.error('commit.analysis.failed', 'Failed', { component: 'commit-reconciler', projectId: 'project-a', operationId: 'op-b', commitSha: 'deadbeef', error: Object.assign(new Error('failure'), { code: 'ETEST' }) });
|
|
21
|
+
await logger.close();
|
|
22
|
+
|
|
23
|
+
const repository = new LogRepository({ layout });
|
|
24
|
+
const result = repository.query({ levels: ['error'], projectId: 'project-a', component: 'commit-reconciler', commitSha: 'deadbeef' });
|
|
25
|
+
assert.strictEqual(result.entries.length, 1);
|
|
26
|
+
assert.strictEqual(result.entries[0].operationId, 'op-b');
|
|
27
|
+
assert.strictEqual(result.entries[0].error.code, 'ETEST');
|
|
28
|
+
assert.strictEqual(result.pageSize, 500, 'today query must default to 500 records');
|
|
29
|
+
assert.strictEqual(repository.query({ pageSize: 999999 }).pageSize, 5000, 'display limit must be capped at 5000');
|
|
30
|
+
assert.strictEqual(repository.findOrphanedOperations().length, 0);
|
|
31
|
+
|
|
32
|
+
await logger.info('migration.started', 'Migration started', { component: 'migration', operationId: 'op-orphan' });
|
|
33
|
+
await logger.close();
|
|
34
|
+
assert.strictEqual(repository.findOrphanedOperations().length, 1);
|
|
35
|
+
|
|
36
|
+
const legacyRoot = layout.getLogPath('app');
|
|
37
|
+
fs.mkdirSync(legacyRoot, { recursive: true });
|
|
38
|
+
fs.writeFileSync(path.join(legacyRoot, '2020-01-01.log'), `${JSON.stringify({ ts: '2020-01-01T00:00:00.000Z', level: 'info', source: 'legacy', event: 'legacy.event', message: 'old' })}\n`);
|
|
39
|
+
assert.strictEqual(repository.query({ from: '2019-01-01', to: '2021-01-01', event: 'legacy.event' }).entries.length, 1, 'legacy app/hooks logs remain read-only query sources');
|
|
40
|
+
|
|
41
|
+
const date = localDay();
|
|
42
|
+
const mergeA = path.join(layout.getLogPath('project', 'merge-a'), `${date}.jsonl`);
|
|
43
|
+
const mergeB = path.join(layout.getLogPath('project', 'merge-b'), `${date}.jsonl`);
|
|
44
|
+
fs.mkdirSync(path.dirname(mergeA), { recursive: true });
|
|
45
|
+
fs.mkdirSync(path.dirname(mergeB), { recursive: true });
|
|
46
|
+
const longMessage = `long-${'界'.repeat(70000)}`;
|
|
47
|
+
const longRecord = { ...normalizeRecord({ id: 'a-1', ts: `${date}T01:00:00.000Z`, level: 'info', event: 'merge.a1', message: 'placeholder', projectId: 'merge-a' }), message: longMessage };
|
|
48
|
+
fs.writeFileSync(mergeA,
|
|
49
|
+
`${JSON.stringify(longRecord)}\r\n`
|
|
50
|
+
+ line({ id: 'a-2', ts: `${date}T03:00:00.000Z`, level: 'warn', event: 'merge.a2', message: 'newer a', projectId: 'merge-a' })
|
|
51
|
+
+ '{bad-tail',
|
|
52
|
+
'utf8');
|
|
53
|
+
fs.writeFileSync(mergeB,
|
|
54
|
+
line({ id: 'b-1', ts: `${date}T02:00:00.000Z`, level: 'error', event: 'merge.b1', message: 'middle b', projectId: 'merge-b' }),
|
|
55
|
+
'utf8');
|
|
56
|
+
|
|
57
|
+
const originalReadFileSync = fs.readFileSync;
|
|
58
|
+
fs.readFileSync = function guardedRead(filePath, ...args) {
|
|
59
|
+
if (typeof filePath === 'string' && filePath.startsWith(path.join(dataDir, 'logs')) && /\.(?:jsonl|log)$/.test(filePath)) {
|
|
60
|
+
throw new Error('LogRepository must not read an entire log file');
|
|
61
|
+
}
|
|
62
|
+
return originalReadFileSync.call(this, filePath, ...args);
|
|
63
|
+
};
|
|
64
|
+
let merged;
|
|
65
|
+
try {
|
|
66
|
+
merged = repository.query({ event: '', q: 'merge.', pageSize: 3 });
|
|
67
|
+
} finally {
|
|
68
|
+
fs.readFileSync = originalReadFileSync;
|
|
69
|
+
}
|
|
70
|
+
assert.deepStrictEqual(merged.entries.map(entry => entry.id), ['a-2', 'b-1', 'a-1'], 'all-project query must k-way merge by timestamp and skip malformed tail data');
|
|
71
|
+
const long = repository.query({ projectId: 'merge-a', event: 'merge.a1' });
|
|
72
|
+
assert.strictEqual(long.entries.length, 1);
|
|
73
|
+
assert(long.entries[0].message.startsWith(`long-${'界'.repeat(100)}`), 'reverse chunk reader must parse lines larger than one chunk');
|
|
74
|
+
assert(long.entries[0].message.includes('[truncated:'), 'public query must still apply field size bounds');
|
|
75
|
+
|
|
76
|
+
const cursorPage = repository.query({ q: 'merge.', pageSize: 1 });
|
|
77
|
+
assert(cursorPage.nextCursor);
|
|
78
|
+
fs.unlinkSync(mergeB);
|
|
79
|
+
assert.throws(
|
|
80
|
+
() => repository.query({ q: 'merge.', pageSize: 1, cursor: cursorPage.nextCursor }),
|
|
81
|
+
error => error.code === 'LOG_CURSOR_EXPIRED' && error.retryable === true,
|
|
82
|
+
'manually deleted cursor source must return a typed restart error',
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
console.log('logging-api-test PASS');
|
|
86
|
+
})().catch(error => {
|
|
87
|
+
console.error(error.stack || error.message);
|
|
88
|
+
process.exitCode = 1;
|
|
89
|
+
});
|