@sanqianx/project-knowledge 4.8.0 → 5.0.0

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.
Files changed (45) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/CHANGELOG.md +19 -0
  3. package/README.md +11 -6
  4. package/_modules/knowledge-engine/README.md +9 -6
  5. package/_modules/knowledge-engine/package.json +1 -1
  6. package/_modules/knowledge-engine/src/coordinator.js +11 -3
  7. package/_modules/knowledge-engine/src/server.js +9 -10
  8. package/_modules/knowledge-engine/src/store.js +9 -12
  9. package/_modules/knowledge-engine/ui/app.js +1 -1
  10. package/_site/_test/fresh-hook-reimport-test.js +53 -0
  11. package/_site/_test/fresh-product-data-test.js +42 -0
  12. package/_site/_test/module-model-configuration-test.js +2 -1
  13. package/_site/_test/tracking-start-test.js +1 -0
  14. package/_site/lib/data-dir.js +13 -81
  15. package/_site/lib/engine-connection.js +67 -0
  16. package/_site/lib/hook-manager.js +1 -1
  17. package/_site/lib/integration-installer.js +2 -1
  18. package/_site/lib/module-bridge.js +6 -16
  19. package/_site/lib/product-data.js +24 -0
  20. package/_site/lib/project-lifecycle-service.js +18 -2
  21. package/_site/lib/server-app.js +11 -22
  22. package/docs/README.zh-CN.md +8 -4
  23. package/docs/fresh-v5-start.md +28 -0
  24. package/module-runtime-manifest.json +2 -2
  25. package/package.json +2 -1
  26. package/plugins/project-knowledge/.claude-plugin/plugin.json +1 -1
  27. package/plugins/project-knowledge/.codex-plugin/plugin.json +1 -1
  28. package/plugins/project-knowledge/.mcp.json +1 -1
  29. package/ui/index.html +2 -1
  30. package/ui/product.js +41 -15
  31. package/vendor-manifest.json +7 -8
  32. package/_modules/knowledge-engine/src/migration.js +0 -88
  33. package/_site/_test/data-dir-migration-test.js +0 -77
  34. package/_site/_test/knowledge-migration-test.js +0 -75
  35. package/_site/_test/legacy-forward-compat-test.js +0 -257
  36. package/_site/_test/legacy-project-upgrade-e2e-test.js +0 -363
  37. package/_site/_test/p0-data-migration-characterization-test.js +0 -37
  38. package/_site/_test/p0-e2e-gate-test.js +0 -347
  39. package/_site/_test/project-layout-v2-migration-test.js +0 -127
  40. package/_site/_test/server-runtime-migration-safety-test.js +0 -27
  41. package/_site/_test/v4122-upgrade-data-contract-test.js +0 -56
  42. package/_site/lib/data-state-classifier.js +0 -40
  43. package/_site/lib/engine-cutover.js +0 -97
  44. package/_site/lib/legacy-data-manifest.js +0 -26
  45. package/_site/lib/migration-service.js +0 -442
@@ -1,363 +0,0 @@
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, analysisOwner: 'legacy', bridgeAdapter: { hookBinding: () => ({ dataDir: layout.getDataDir() }) } };
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, analysisOwner: 'legacy', bridgeAdapter: { hookBinding: () => ({ dataDir: layout.getDataDir() }) } };
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, analysisOwner: 'legacy', bridgeAdapter: { hookBinding: () => ({ dataDir: layout.getDataDir() }) } };
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, analysisOwner: 'legacy', bridgeAdapter: { hookBinding: () => ({ dataDir: layout.getDataDir() }) } };
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
- });
@@ -1,37 +0,0 @@
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 { MigrationService } = require('../lib/migration-service');
7
-
8
- function fixture() {
9
- const dataDir = fs.mkdtempSync(path.join(os.tmpdir(), 'pk-p0-migration-'));
10
- fs.writeFileSync(path.join(dataDir, 'projects.json'), JSON.stringify({ old: { repoPath: 'C:/legacy', kbPath: 'C:/knowledge/old' } }));
11
- fs.writeFileSync(path.join(dataDir, 'knowledge-store.json'), JSON.stringify({ rootPath: 'C:/knowledge' }));
12
- fs.writeFileSync(path.join(dataDir, 'ai-profiles.json'), JSON.stringify({ profiles: [{ id: 'p', apiKey: 'test-secret' }] }));
13
- fs.writeFileSync(path.join(dataDir, 'embedding-config.json'), JSON.stringify({ modelId: 'legacy-model', localModelPath: 'C:/models', localFilesOnly: true }));
14
- fs.mkdirSync(path.join(dataDir, 'models', 'nested'), { recursive: true });
15
- fs.writeFileSync(path.join(dataDir, 'models', 'nested', 'model.onnx'), 'model-fixture');
16
- return dataDir;
17
- }
18
-
19
- (async () => {
20
- const dataDir = fixture();
21
- try {
22
- const layout = new StorageLayout({ dataDir });
23
- const migration = new MigrationService({ layout, legacyDataDir: dataDir });
24
- const result = await migration.migrateIfNeeded({ migrationRunId: 'p0-characterization' });
25
- assert.equal(result.ok, true);
26
- assert.equal(result.marker.schema, 'layout-migration-completion/v2');
27
- assert.equal(result.marker.projectCount, 1);
28
- assert.equal(result.marker.aiProfileCount, 1);
29
- assert.equal(result.marker.knowledgeRootConfigured, true);
30
- assert.equal(result.marker.embeddingConfigured, true);
31
- assert.equal(fs.readFileSync(path.join(dataDir, 'cache', 'models', 'nested', 'model.onnx'), 'utf8'), 'model-fixture');
32
- const settings = JSON.parse(fs.readFileSync(layout.getSettingsPath(), 'utf8'));
33
- assert.equal(settings.embedding.modelId, 'legacy-model');
34
- assert.equal(settings.ai.profiles.length, 1);
35
- } finally { fs.rmSync(dataDir, { recursive: true, force: true }); }
36
- console.log('p0-data-migration-characterization-test PASS');
37
- })().catch(error => { console.error(error.stack || error.message); process.exit(1); });