@sanqianx/project-knowledge 4.6.1 → 4.8.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.
- package/.agents/plugins/marketplace.json +17 -17
- package/.claude-plugin/marketplace.json +1 -1
- package/CHANGELOG.md +817 -780
- package/README.md +48 -36
- package/_modules/knowledge-engine/README.md +88 -0
- package/_modules/knowledge-engine/package.json +38 -0
- package/_modules/knowledge-engine/src/analysis-contract.js +25 -0
- package/_modules/knowledge-engine/src/analysis-worker.js +54 -0
- package/_modules/knowledge-engine/src/bin.js +52 -0
- package/_modules/knowledge-engine/src/bridge-client.js +24 -0
- package/_modules/knowledge-engine/src/coordinator.js +255 -0
- package/_modules/knowledge-engine/src/headless-analyzer.js +75 -0
- package/_modules/knowledge-engine/src/memories.js +85 -0
- package/_modules/knowledge-engine/src/migration.js +88 -0
- package/_modules/knowledge-engine/src/retrieval.js +116 -0
- package/_modules/knowledge-engine/src/runtime/README.md +7 -0
- package/_modules/knowledge-engine/src/runtime/atomic-file.js +204 -0
- package/_modules/knowledge-engine/src/runtime/automation-config.js +140 -0
- package/_modules/knowledge-engine/src/runtime/commit-conversation-binder.js +249 -0
- package/_modules/knowledge-engine/src/runtime/commit-processing-ledger.js +26 -0
- package/_modules/knowledge-engine/src/runtime/commit-prompt.js +202 -0
- package/_modules/knowledge-engine/src/runtime/commit-reconciler.js +432 -0
- package/_modules/knowledge-engine/src/runtime/content-hash.js +4 -0
- package/_modules/knowledge-engine/src/runtime/contracts.js +279 -0
- package/_modules/knowledge-engine/src/runtime/conversation-exclusions.js +204 -0
- package/_modules/knowledge-engine/src/runtime/conversation-store.js +387 -0
- package/_modules/knowledge-engine/src/runtime/evidence-bundle.js +228 -0
- package/_modules/knowledge-engine/src/runtime/git-runner.js +62 -0
- package/_modules/knowledge-engine/src/runtime/knowledge-promotion.js +444 -0
- package/_modules/knowledge-engine/src/runtime/knowledge-retrieval-service.js +402 -0
- package/_modules/knowledge-engine/src/runtime/layout.js +44 -0
- package/_modules/knowledge-engine/src/runtime/markdown-knowledge-indexer.js +230 -0
- package/_modules/knowledge-engine/src/runtime/scanner.js +298 -0
- package/_modules/knowledge-engine/src/server.js +155 -0
- package/_modules/knowledge-engine/src/store.js +239 -0
- package/_modules/knowledge-engine/ui/app.css +172 -0
- package/_modules/knowledge-engine/ui/app.js +297 -0
- package/_modules/knowledge-engine/ui/index.html +95 -0
- package/_site/README.md +30 -30
- package/_site/_test/ai-profile-resolver-test.js +137 -137
- package/_site/_test/automation-queue-test.js +14 -14
- package/_site/_test/automation-ui-test.js +40 -40
- package/_site/_test/background-task-registry-test.js +43 -43
- package/_site/_test/baseline-schema-test.js +91 -91
- package/_site/_test/bridge-adapter-test.js +1 -1
- package/_site/_test/bridge-consumer-service-test.js +1 -1
- package/_site/_test/chat-claudecodeui-match-test.js +92 -92
- package/_site/_test/claude-executable-discovery-test.js +62 -62
- package/_site/_test/claude-workbench-test.js +170 -170
- package/_site/_test/client-capture-restart-e2e-test.js +4 -4
- package/_site/_test/codex-conversation-projection-test.js +2 -1
- package/_site/_test/commit-boundary-freeze-test.js +1 -1
- package/_site/_test/commit-conversation-binding-test.js +95 -95
- package/_site/_test/commit-evidence-test.js +110 -110
- package/_site/_test/conversation-api-test.js +109 -109
- package/_site/_test/conversation-store-test.js +136 -136
- package/_site/_test/cross-repo-isolation-e2e-test.js +2 -2
- package/_site/_test/data-dir-migration-test.js +57 -57
- package/_site/_test/desktop-browser-compat-test.js +28 -28
- package/_site/_test/desktop-hook-runtime-regression-test.js +204 -204
- package/_site/_test/explicit-commit-processor-test.js +37 -37
- package/_site/_test/fixtures/make-git-repos.js +12 -1
- package/_site/_test/folder-picker-output-test.js +19 -19
- package/_site/_test/full-integration-e2e-test.js +165 -165
- package/_site/_test/git-validation-test.js +2 -2
- package/_site/_test/hook-runtime-endpoint-test.js +88 -88
- package/_site/_test/hook-status-repair-api-test.js +152 -152
- package/_site/_test/hook-trigger-test.js +85 -85
- package/_site/_test/import-preflight-api-test.js +234 -233
- package/_site/_test/index-writer-concurrency-test.js +135 -135
- package/_site/_test/integration-adapters-test.js +171 -171
- package/_site/_test/integration-capture-setup-test.js +1 -1
- package/_site/_test/integration-surface-coverage-test.js +164 -164
- package/_site/_test/knowledge-language-control-test.js +170 -170
- package/_site/_test/knowledge-promotion-recovery-test.js +260 -260
- package/_site/_test/knowledge-query-test.js +54 -54
- package/_site/_test/knowledge-retrieval-service-test.js +77 -77
- package/_site/_test/knowledge-storage-startup-test.js +52 -52
- package/_site/_test/knowledge-store-logs-supervision-test.js +89 -89
- package/_site/_test/legacy-forward-compat-test.js +257 -257
- package/_site/_test/legacy-project-upgrade-e2e-test.js +363 -363
- package/_site/_test/logging-api-test.js +89 -89
- package/_site/_test/logging-sse-no-gap-test.js +88 -88
- package/_site/_test/logging-ui-test.js +106 -104
- package/_site/_test/markdown-delta-overlay-test.js +85 -85
- package/_site/_test/markdown-maintenance-api-test.js +75 -75
- package/_site/_test/mcp-server-test.js +149 -149
- package/_site/_test/module-artifact-integrity-test.js +27 -0
- package/_site/_test/module-boundary-test.js +30 -0
- package/_site/_test/module-bridge-eventbridge-test.js +7 -0
- package/_site/_test/module-model-configuration-test.js +43 -0
- package/_site/_test/module-process-lifecycle-test.js +24 -0
- package/_site/_test/module-stream-proxy-test.js +34 -0
- package/_site/_test/non-release-ci-test.js +34 -34
- package/_site/_test/offline-boundary-isolation-test.js +32 -32
- package/_site/_test/p0-data-migration-characterization-test.js +37 -37
- package/_site/_test/p0-e2e-gate-test.js +347 -347
- package/_site/_test/packaged-ui-smoke-test.js +19 -10
- package/_site/_test/path-consistency-test.js +151 -151
- package/_site/_test/pending-sweep-test.js +9 -9
- package/_site/_test/post-commit-automation-test.js +87 -87
- package/_site/_test/product-diagnostics-ui-test.js +75 -0
- package/_site/_test/product-import-ui-test.js +92 -0
- package/_site/_test/project-delete-recovery-test.js +64 -64
- package/_site/_test/project-goal-editor-test.js +144 -144
- package/_site/_test/project-layout-v2-migration-test.js +127 -127
- package/_site/_test/project-lifecycle-transaction-test.js +106 -106
- package/_site/_test/prompt-settings-test.js +115 -115
- package/_site/_test/protected-architecture-gate-test.js +122 -122
- package/_site/_test/refactor-characterization-test.js +36 -36
- package/_site/_test/release-version-sync-test.js +1 -1
- package/_site/_test/requirement-recorder-test.js +173 -173
- package/_site/_test/run-all-tests.js +159 -156
- package/_site/_test/server-runtime-migration-safety-test.js +27 -27
- package/_site/_test/shared-contracts-test.js +58 -58
- package/_site/_test/startup-analysis-disabled-test.js +10 -10
- package/_site/_test/storage-foundation-test.js +78 -78
- package/_site/_test/structured-logger-test.js +101 -101
- package/_site/_test/tracking-start-test.js +108 -108
- package/_site/_test/v4122-upgrade-data-contract-test.js +56 -56
- package/_site/_test/workbench-permission-test.js +117 -117
- package/_site/_test/workspace-ui-contract-test.js +28 -43
- package/_site/lib/ai-profile-resolver.js +78 -78
- package/_site/lib/ai-workspace.js +101 -101
- package/_site/lib/automation-config.js +140 -140
- package/_site/lib/bridge-adapter.js +5 -1
- package/_site/lib/bridge-consumer-service.js +402 -402
- package/_site/lib/claude-cli-runner.js +1510 -1510
- package/_site/lib/commit-conversation-binder.js +234 -234
- package/_site/lib/commit-processing-ledger.js +27 -27
- package/_site/lib/commit-prompt.js +202 -202
- package/_site/lib/commit-reconciler.js +406 -406
- package/_site/lib/contracts.js +284 -279
- package/_site/lib/conversation-query-service.js +144 -144
- package/_site/lib/conversation-store.js +385 -385
- package/_site/lib/data-dir.js +74 -74
- package/_site/lib/data-state-classifier.js +40 -40
- package/_site/lib/engine-cutover.js +97 -0
- package/_site/lib/evidence-bundle.js +228 -228
- package/_site/lib/folder-picker-output.js +21 -21
- package/_site/lib/git-runner.js +62 -62
- package/_site/lib/github-team-store.js +1172 -1172
- package/_site/lib/hook-manager.js +203 -202
- package/_site/lib/index-service.js +173 -173
- package/_site/lib/integration-installer.js +907 -907
- package/_site/lib/kb-framework.js +176 -176
- package/_site/lib/kb-validator.js +124 -124
- package/_site/lib/knowledge-promotion.js +435 -435
- package/_site/lib/knowledge-retrieval-service.js +401 -401
- package/_site/lib/knowledge-tool-runtime.js +359 -359
- package/_site/lib/legacy-data-manifest.js +26 -26
- package/_site/lib/llm-client.js +161 -161
- package/_site/lib/markdown-knowledge-indexer.js +230 -230
- package/_site/lib/migration-service.js +442 -442
- package/_site/lib/module-bridge.js +198 -33
- package/_site/lib/post-commit-automation.js +87 -87
- package/_site/lib/project-lifecycle-service.js +519 -497
- package/_site/lib/project-store.js +268 -262
- package/_site/lib/requirement-recorder.js +278 -278
- package/_site/lib/runtime-endpoint.js +155 -155
- package/_site/lib/scanner.js +298 -298
- package/_site/lib/server-app.js +1590 -1520
- package/_site/lib/settings-store.js +133 -113
- package/_site/lib/storage-layout.js +162 -162
- package/_site/lib/structured-logger.js +574 -574
- package/_site/scripts/folder-picker.ps1 +156 -156
- package/_site/scripts/hook-trigger.js +157 -156
- package/_site/scripts/install-module-candidates.js +69 -0
- package/_site/scripts/pack-module-candidates.js +72 -0
- package/_site/scripts/sync-release-version.js +2 -2
- package/_site/scripts/vendor-modules.js +47 -0
- package/_site/scripts/verify-module-runtime.js +45 -0
- package/_site/scripts/verify-product-runtime.js +34 -0
- package/bin/project-knowledge-mcp.js +194 -194
- package/docs/README.zh-CN.md +34 -25
- package/docs/project-registry-schema.md +22 -22
- package/docs/testing-strategy.md +33 -33
- package/module-runtime-manifest.json +220 -0
- package/package.json +13 -10
- package/plugins/project-knowledge/.claude-plugin/plugin.json +1 -1
- package/plugins/project-knowledge/.codex-plugin/plugin.json +1 -1
- package/plugins/project-knowledge/.mcp.json +1 -1
- package/plugins/project-knowledge/opencode/project-knowledge.md +3 -3
- package/plugins/project-knowledge/skills/project-knowledge/SKILL.md +28 -28
- package/templates/project-readme.md +34 -34
- package/ui/favicon.svg +38 -38
- package/ui/index.html +51 -149
- package/ui/product.css +9 -0
- package/ui/product.js +386 -0
- package/vendor-manifest.json +46 -0
- package/_site/_test/import-ui-flow-test.js +0 -171
- package/_site/_test/project-control-panel-task14-test.js +0 -63
- package/_site/_test/task15-20-ui-flow-test.js +0 -148
- package/_site/_test/ui-i18n-toggle-test.js +0 -114
- package/_site/_test/ui-smoke-test.js +0 -73
- package/ui/app.css +0 -58
- package/ui/app.js +0 -607
- package/ui/i18n.js +0 -146
|
@@ -1,127 +1,127 @@
|
|
|
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-layout-v2-migration-'));
|
|
10
|
-
const oldRoot = path.join(dataDir, 'old-knowledge');
|
|
11
|
-
const differentRoot = path.join(dataDir, 'different-current-root');
|
|
12
|
-
fs.mkdirSync(oldRoot, { recursive: true });
|
|
13
|
-
const exactKbPath = path.join(oldRoot, 'legacy-a-fixed');
|
|
14
|
-
fs.mkdirSync(exactKbPath, { recursive: true });
|
|
15
|
-
fs.writeFileSync(path.join(exactKbPath, 'README.md'), '# existing knowledge\n', 'utf8');
|
|
16
|
-
fs.writeFileSync(path.join(dataDir, 'projects.json'), JSON.stringify({
|
|
17
|
-
legacyA: {
|
|
18
|
-
displayName: 'Legacy A',
|
|
19
|
-
localPath: path.join(dataDir, 'repo-a'),
|
|
20
|
-
gitPath: path.join(dataDir, 'repo-a'),
|
|
21
|
-
kbPath: exactKbPath,
|
|
22
|
-
trackingStartCommit: 'track-a',
|
|
23
|
-
lastAnalyzedCommit: 'last-a',
|
|
24
|
-
enabled: true,
|
|
25
|
-
aiProfileId: 'primary',
|
|
26
|
-
},
|
|
27
|
-
legacyB: {
|
|
28
|
-
displayName: 'Legacy B',
|
|
29
|
-
localPath: path.join(dataDir, 'repo-b'),
|
|
30
|
-
trackingStartCommit: 'track-b',
|
|
31
|
-
enabled: true,
|
|
32
|
-
},
|
|
33
|
-
}, null, 2));
|
|
34
|
-
fs.writeFileSync(path.join(dataDir, 'knowledge-store.json'), JSON.stringify({ rootPath: differentRoot, configured: true }));
|
|
35
|
-
fs.writeFileSync(path.join(dataDir, 'ai-profiles.json'), JSON.stringify({ schema: 'ai-profiles/v1', profiles: [{ id: 'primary', apiKey: 'exact-secret-value' }] }));
|
|
36
|
-
fs.writeFileSync(path.join(dataDir, 'embedding-config.json'), JSON.stringify({ modelId: 'model-a' }));
|
|
37
|
-
fs.writeFileSync(path.join(dataDir, 'logging.json'), JSON.stringify({ levels: ['debug', 'info', 'error'], retentionDays: 0, maxTotalSizeMB: 512 }));
|
|
38
|
-
fs.writeFileSync(path.join(dataDir, 'claude-prompts.json'), JSON.stringify({ hookPromptTemplate: 'legacy override' }));
|
|
39
|
-
fs.writeFileSync(path.join(dataDir, '.hook-trigger-errors.log'), '2026-01-01 old hook error\n');
|
|
40
|
-
return { dataDir, exactKbPath, differentRoot };
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
(async () => {
|
|
44
|
-
{
|
|
45
|
-
const { dataDir, exactKbPath, differentRoot } = fixture();
|
|
46
|
-
const layout = new StorageLayout({ dataDir });
|
|
47
|
-
const migration = new MigrationService({ layout, legacyDataDir: dataDir });
|
|
48
|
-
const result = await migration.migrateIfNeeded({ migrationRunId: 'layout-v2-success' });
|
|
49
|
-
assert.strictEqual(result.ok, true);
|
|
50
|
-
assert.strictEqual(result.completed, true);
|
|
51
|
-
assert(fs.existsSync(layout.getMigrationCompletionPath()));
|
|
52
|
-
const registry = JSON.parse(fs.readFileSync(layout.getProjectRegistryPath(), 'utf8'));
|
|
53
|
-
assert.strictEqual(registry.schema, 'project-registry/v2');
|
|
54
|
-
assert.strictEqual(registry.projectOrder.length, 2);
|
|
55
|
-
const idA = result.projectMap.legacyA;
|
|
56
|
-
const idB = result.projectMap.legacyB;
|
|
57
|
-
const configA = JSON.parse(fs.readFileSync(layout.getProjectConfigPath(idA), 'utf8'));
|
|
58
|
-
const configB = JSON.parse(fs.readFileSync(layout.getProjectConfigPath(idB), 'utf8'));
|
|
59
|
-
const stateA = JSON.parse(fs.readFileSync(layout.getProjectStatePath(idA), 'utf8'));
|
|
60
|
-
assert.strictEqual(configA.knowledgePath, exactKbPath, 'old kbPath must remain exact');
|
|
61
|
-
assert.strictEqual(configB.knowledgePath, path.join(differentRoot, 'legacyB'));
|
|
62
|
-
assert.strictEqual(stateA.trackingStartCommit, 'track-a');
|
|
63
|
-
assert.strictEqual(stateA.lastAnalyzedCommit, 'last-a');
|
|
64
|
-
const settings = JSON.parse(fs.readFileSync(layout.getSettingsPath(), 'utf8'));
|
|
65
|
-
assert.strictEqual(settings.ai.profiles[0].apiKey, 'exact-secret-value');
|
|
66
|
-
assert.strictEqual(Object.prototype.hasOwnProperty.call(settings.logging, 'retentionDays'), false);
|
|
67
|
-
assert.strictEqual(settings.legacyExtensions.logging.retentionDays, 0, 'legacy logging settings remain preserved as read-only migration evidence');
|
|
68
|
-
assert(fs.existsSync(path.join(result.recoveryDir, 'backup', 'projects.json')));
|
|
69
|
-
assert(fs.existsSync(path.join(dataDir, '.hook-trigger-errors.log')), 'legacy hook log must remain');
|
|
70
|
-
const second = await migration.migrateIfNeeded();
|
|
71
|
-
assert.strictEqual(second.completed, true);
|
|
72
|
-
assert.strictEqual(second.migrated, false);
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
for (const stage of ['backup', 'staging', 'validation', 'activation', 'open-verification']) {
|
|
76
|
-
const { dataDir } = fixture();
|
|
77
|
-
const originalProjects = fs.readFileSync(path.join(dataDir, 'projects.json'), 'utf8');
|
|
78
|
-
const layout = new StorageLayout({ dataDir });
|
|
79
|
-
const migration = new MigrationService({ layout, legacyDataDir: dataDir });
|
|
80
|
-
const result = await migration.migrateIfNeeded({ migrationRunId: `fault-${stage}`, faultAt: stage });
|
|
81
|
-
assert.strictEqual(result.ok, false, `${stage} should fail`);
|
|
82
|
-
assert.strictEqual(result.useLegacy, true);
|
|
83
|
-
assert.strictEqual(fs.existsSync(layout.getMigrationCompletionPath()), false, `${stage} must not write completion`);
|
|
84
|
-
assert.strictEqual(fs.readFileSync(path.join(dataDir, 'projects.json'), 'utf8'), originalProjects, `${stage} must preserve/restore legacy registry`);
|
|
85
|
-
const journal = JSON.parse(fs.readFileSync(path.join(result.recoveryDir, 'journal.json'), 'utf8'));
|
|
86
|
-
assert.strictEqual(journal.phase, 'failed');
|
|
87
|
-
assert.strictEqual(journal.rollback.ok, true, `${stage} rollback must complete`);
|
|
88
|
-
assert.strictEqual(fs.existsSync(layout.getSettingsPath()), false, `${stage} must remove newly activated settings`);
|
|
89
|
-
assert.strictEqual(fs.existsSync(path.join(dataDir, 'projects')), false, `${stage} must remove newly activated project metadata`);
|
|
90
|
-
if (['activation', 'open-verification'].includes(stage)) {
|
|
91
|
-
const retry = await migration.migrateIfNeeded({ migrationRunId: `retry-${stage}` });
|
|
92
|
-
assert.strictEqual(retry.ok, true, `${stage} must be safely retryable after rollback`);
|
|
93
|
-
assert.strictEqual(retry.completed, true);
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
{
|
|
98
|
-
const dataDir = fs.mkdtempSync(path.join(os.tmpdir(), 'pk-layout-v2-fresh-'));
|
|
99
|
-
const layout = new StorageLayout({ dataDir });
|
|
100
|
-
const result = await new MigrationService({ layout }).migrateIfNeeded();
|
|
101
|
-
assert.strictEqual(result.reason, 'fresh-install');
|
|
102
|
-
assert.strictEqual(fs.existsSync(layout.getRecoveryPath()), false, 'fresh install must not create optional recovery dir');
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
{
|
|
106
|
-
// Regression: legacy files carrying a UTF-8 BOM (common from PowerShell
|
|
107
|
-
// tooling) must migrate cleanly instead of failing as corrupt sources.
|
|
108
|
-
const dataDir = fs.mkdtempSync(path.join(os.tmpdir(), 'pk-layout-v2-bom-'));
|
|
109
|
-
const layout = new StorageLayout({ dataDir });
|
|
110
|
-
const bom = Buffer.concat([Buffer.from([0xef, 0xbb, 0xbf]), Buffer.from(JSON.stringify({
|
|
111
|
-
legacyA: { displayName: 'BOM A', localPath: path.join(dataDir, 'repo-a'), trackingStartCommit: 'track-a', enabled: true }
|
|
112
|
-
}), 'utf8')]);
|
|
113
|
-
fs.writeFileSync(path.join(dataDir, 'projects.json'), bom);
|
|
114
|
-
fs.writeFileSync(path.join(dataDir, 'team-git-providers.json'), Buffer.concat([Buffer.from([0xef, 0xbb, 0xbf]), Buffer.from('{}', 'utf8')]));
|
|
115
|
-
const result = await new MigrationService({ layout, legacyDataDir: dataDir }).migrateIfNeeded();
|
|
116
|
-
assert.strictEqual(result.ok, true, 'BOM-prefixed legacy sources must migrate');
|
|
117
|
-
assert.strictEqual(result.migrated, true);
|
|
118
|
-
const registry = JSON.parse(fs.readFileSync(layout.getProjectRegistryPath(), 'utf8'));
|
|
119
|
-
assert.strictEqual(registry.schema, 'project-registry/v2');
|
|
120
|
-
assert.strictEqual(Object.keys(registry.projects).length, 1);
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
console.log('project-layout-v2-migration-test PASS');
|
|
124
|
-
})().catch(error => {
|
|
125
|
-
console.error(error.stack || error.message);
|
|
126
|
-
process.exitCode = 1;
|
|
127
|
-
});
|
|
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-layout-v2-migration-'));
|
|
10
|
+
const oldRoot = path.join(dataDir, 'old-knowledge');
|
|
11
|
+
const differentRoot = path.join(dataDir, 'different-current-root');
|
|
12
|
+
fs.mkdirSync(oldRoot, { recursive: true });
|
|
13
|
+
const exactKbPath = path.join(oldRoot, 'legacy-a-fixed');
|
|
14
|
+
fs.mkdirSync(exactKbPath, { recursive: true });
|
|
15
|
+
fs.writeFileSync(path.join(exactKbPath, 'README.md'), '# existing knowledge\n', 'utf8');
|
|
16
|
+
fs.writeFileSync(path.join(dataDir, 'projects.json'), JSON.stringify({
|
|
17
|
+
legacyA: {
|
|
18
|
+
displayName: 'Legacy A',
|
|
19
|
+
localPath: path.join(dataDir, 'repo-a'),
|
|
20
|
+
gitPath: path.join(dataDir, 'repo-a'),
|
|
21
|
+
kbPath: exactKbPath,
|
|
22
|
+
trackingStartCommit: 'track-a',
|
|
23
|
+
lastAnalyzedCommit: 'last-a',
|
|
24
|
+
enabled: true,
|
|
25
|
+
aiProfileId: 'primary',
|
|
26
|
+
},
|
|
27
|
+
legacyB: {
|
|
28
|
+
displayName: 'Legacy B',
|
|
29
|
+
localPath: path.join(dataDir, 'repo-b'),
|
|
30
|
+
trackingStartCommit: 'track-b',
|
|
31
|
+
enabled: true,
|
|
32
|
+
},
|
|
33
|
+
}, null, 2));
|
|
34
|
+
fs.writeFileSync(path.join(dataDir, 'knowledge-store.json'), JSON.stringify({ rootPath: differentRoot, configured: true }));
|
|
35
|
+
fs.writeFileSync(path.join(dataDir, 'ai-profiles.json'), JSON.stringify({ schema: 'ai-profiles/v1', profiles: [{ id: 'primary', apiKey: 'exact-secret-value' }] }));
|
|
36
|
+
fs.writeFileSync(path.join(dataDir, 'embedding-config.json'), JSON.stringify({ modelId: 'model-a' }));
|
|
37
|
+
fs.writeFileSync(path.join(dataDir, 'logging.json'), JSON.stringify({ levels: ['debug', 'info', 'error'], retentionDays: 0, maxTotalSizeMB: 512 }));
|
|
38
|
+
fs.writeFileSync(path.join(dataDir, 'claude-prompts.json'), JSON.stringify({ hookPromptTemplate: 'legacy override' }));
|
|
39
|
+
fs.writeFileSync(path.join(dataDir, '.hook-trigger-errors.log'), '2026-01-01 old hook error\n');
|
|
40
|
+
return { dataDir, exactKbPath, differentRoot };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
(async () => {
|
|
44
|
+
{
|
|
45
|
+
const { dataDir, exactKbPath, differentRoot } = fixture();
|
|
46
|
+
const layout = new StorageLayout({ dataDir });
|
|
47
|
+
const migration = new MigrationService({ layout, legacyDataDir: dataDir });
|
|
48
|
+
const result = await migration.migrateIfNeeded({ migrationRunId: 'layout-v2-success' });
|
|
49
|
+
assert.strictEqual(result.ok, true);
|
|
50
|
+
assert.strictEqual(result.completed, true);
|
|
51
|
+
assert(fs.existsSync(layout.getMigrationCompletionPath()));
|
|
52
|
+
const registry = JSON.parse(fs.readFileSync(layout.getProjectRegistryPath(), 'utf8'));
|
|
53
|
+
assert.strictEqual(registry.schema, 'project-registry/v2');
|
|
54
|
+
assert.strictEqual(registry.projectOrder.length, 2);
|
|
55
|
+
const idA = result.projectMap.legacyA;
|
|
56
|
+
const idB = result.projectMap.legacyB;
|
|
57
|
+
const configA = JSON.parse(fs.readFileSync(layout.getProjectConfigPath(idA), 'utf8'));
|
|
58
|
+
const configB = JSON.parse(fs.readFileSync(layout.getProjectConfigPath(idB), 'utf8'));
|
|
59
|
+
const stateA = JSON.parse(fs.readFileSync(layout.getProjectStatePath(idA), 'utf8'));
|
|
60
|
+
assert.strictEqual(configA.knowledgePath, exactKbPath, 'old kbPath must remain exact');
|
|
61
|
+
assert.strictEqual(configB.knowledgePath, path.join(differentRoot, 'legacyB'));
|
|
62
|
+
assert.strictEqual(stateA.trackingStartCommit, 'track-a');
|
|
63
|
+
assert.strictEqual(stateA.lastAnalyzedCommit, 'last-a');
|
|
64
|
+
const settings = JSON.parse(fs.readFileSync(layout.getSettingsPath(), 'utf8'));
|
|
65
|
+
assert.strictEqual(settings.ai.profiles[0].apiKey, 'exact-secret-value');
|
|
66
|
+
assert.strictEqual(Object.prototype.hasOwnProperty.call(settings.logging, 'retentionDays'), false);
|
|
67
|
+
assert.strictEqual(settings.legacyExtensions.logging.retentionDays, 0, 'legacy logging settings remain preserved as read-only migration evidence');
|
|
68
|
+
assert(fs.existsSync(path.join(result.recoveryDir, 'backup', 'projects.json')));
|
|
69
|
+
assert(fs.existsSync(path.join(dataDir, '.hook-trigger-errors.log')), 'legacy hook log must remain');
|
|
70
|
+
const second = await migration.migrateIfNeeded();
|
|
71
|
+
assert.strictEqual(second.completed, true);
|
|
72
|
+
assert.strictEqual(second.migrated, false);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
for (const stage of ['backup', 'staging', 'validation', 'activation', 'open-verification']) {
|
|
76
|
+
const { dataDir } = fixture();
|
|
77
|
+
const originalProjects = fs.readFileSync(path.join(dataDir, 'projects.json'), 'utf8');
|
|
78
|
+
const layout = new StorageLayout({ dataDir });
|
|
79
|
+
const migration = new MigrationService({ layout, legacyDataDir: dataDir });
|
|
80
|
+
const result = await migration.migrateIfNeeded({ migrationRunId: `fault-${stage}`, faultAt: stage });
|
|
81
|
+
assert.strictEqual(result.ok, false, `${stage} should fail`);
|
|
82
|
+
assert.strictEqual(result.useLegacy, true);
|
|
83
|
+
assert.strictEqual(fs.existsSync(layout.getMigrationCompletionPath()), false, `${stage} must not write completion`);
|
|
84
|
+
assert.strictEqual(fs.readFileSync(path.join(dataDir, 'projects.json'), 'utf8'), originalProjects, `${stage} must preserve/restore legacy registry`);
|
|
85
|
+
const journal = JSON.parse(fs.readFileSync(path.join(result.recoveryDir, 'journal.json'), 'utf8'));
|
|
86
|
+
assert.strictEqual(journal.phase, 'failed');
|
|
87
|
+
assert.strictEqual(journal.rollback.ok, true, `${stage} rollback must complete`);
|
|
88
|
+
assert.strictEqual(fs.existsSync(layout.getSettingsPath()), false, `${stage} must remove newly activated settings`);
|
|
89
|
+
assert.strictEqual(fs.existsSync(path.join(dataDir, 'projects')), false, `${stage} must remove newly activated project metadata`);
|
|
90
|
+
if (['activation', 'open-verification'].includes(stage)) {
|
|
91
|
+
const retry = await migration.migrateIfNeeded({ migrationRunId: `retry-${stage}` });
|
|
92
|
+
assert.strictEqual(retry.ok, true, `${stage} must be safely retryable after rollback`);
|
|
93
|
+
assert.strictEqual(retry.completed, true);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
{
|
|
98
|
+
const dataDir = fs.mkdtempSync(path.join(os.tmpdir(), 'pk-layout-v2-fresh-'));
|
|
99
|
+
const layout = new StorageLayout({ dataDir });
|
|
100
|
+
const result = await new MigrationService({ layout }).migrateIfNeeded();
|
|
101
|
+
assert.strictEqual(result.reason, 'fresh-install');
|
|
102
|
+
assert.strictEqual(fs.existsSync(layout.getRecoveryPath()), false, 'fresh install must not create optional recovery dir');
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
{
|
|
106
|
+
// Regression: legacy files carrying a UTF-8 BOM (common from PowerShell
|
|
107
|
+
// tooling) must migrate cleanly instead of failing as corrupt sources.
|
|
108
|
+
const dataDir = fs.mkdtempSync(path.join(os.tmpdir(), 'pk-layout-v2-bom-'));
|
|
109
|
+
const layout = new StorageLayout({ dataDir });
|
|
110
|
+
const bom = Buffer.concat([Buffer.from([0xef, 0xbb, 0xbf]), Buffer.from(JSON.stringify({
|
|
111
|
+
legacyA: { displayName: 'BOM A', localPath: path.join(dataDir, 'repo-a'), trackingStartCommit: 'track-a', enabled: true }
|
|
112
|
+
}), 'utf8')]);
|
|
113
|
+
fs.writeFileSync(path.join(dataDir, 'projects.json'), bom);
|
|
114
|
+
fs.writeFileSync(path.join(dataDir, 'team-git-providers.json'), Buffer.concat([Buffer.from([0xef, 0xbb, 0xbf]), Buffer.from('{}', 'utf8')]));
|
|
115
|
+
const result = await new MigrationService({ layout, legacyDataDir: dataDir }).migrateIfNeeded();
|
|
116
|
+
assert.strictEqual(result.ok, true, 'BOM-prefixed legacy sources must migrate');
|
|
117
|
+
assert.strictEqual(result.migrated, true);
|
|
118
|
+
const registry = JSON.parse(fs.readFileSync(layout.getProjectRegistryPath(), 'utf8'));
|
|
119
|
+
assert.strictEqual(registry.schema, 'project-registry/v2');
|
|
120
|
+
assert.strictEqual(Object.keys(registry.projects).length, 1);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
console.log('project-layout-v2-migration-test PASS');
|
|
124
|
+
})().catch(error => {
|
|
125
|
+
console.error(error.stack || error.message);
|
|
126
|
+
process.exitCode = 1;
|
|
127
|
+
});
|
|
@@ -1,106 +1,106 @@
|
|
|
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
|
-
const { StorageLayout } = require('../lib/storage-layout');
|
|
7
|
-
const { SettingsStore } = require('../lib/settings-store');
|
|
8
|
-
const { ProjectRegistryStore } = require('../lib/project-registry-store');
|
|
9
|
-
const { ProjectStore } = require('../lib/project-store');
|
|
10
|
-
const { ProjectLifecycleService } = require('../lib/project-lifecycle-service');
|
|
11
|
-
const realHookManager = require('../lib/hook-manager');
|
|
12
|
-
|
|
13
|
-
const ROOT = path.resolve(__dirname, '..', '..');
|
|
14
|
-
const TRIGGER = path.join(ROOT, '_site', 'scripts', 'hook-trigger.js');
|
|
15
|
-
|
|
16
|
-
function git(repo, args) {
|
|
17
|
-
const result = spawnSync('git', ['-C', repo, ...args], { encoding: 'utf8', windowsHide: true, env: { ...process.env, GIT_TERMINAL_PROMPT: '0' } });
|
|
18
|
-
if (result.status !== 0) throw new Error(result.stderr || `git ${args.join(' ')} failed`);
|
|
19
|
-
return String(result.stdout || '').trim();
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
function createRepo(root, name, withCommit = true) {
|
|
23
|
-
const repo = path.join(root, name);
|
|
24
|
-
fs.mkdirSync(repo, { recursive: true });
|
|
25
|
-
git(repo, ['init', '--initial-branch=main']);
|
|
26
|
-
git(repo, ['config', 'user.email', 'life@example.invalid']);
|
|
27
|
-
git(repo, ['config', 'user.name', 'Lifecycle Test']);
|
|
28
|
-
if (withCommit) {
|
|
29
|
-
fs.writeFileSync(path.join(repo, 'README.md'), `# ${name}\n`);
|
|
30
|
-
git(repo, ['add', 'README.md']);
|
|
31
|
-
git(repo, ['commit', '-m', 'initial']);
|
|
32
|
-
}
|
|
33
|
-
return repo;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
(async () => {
|
|
37
|
-
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'pk-lifecycle-'));
|
|
38
|
-
const dataDir = path.join(root, 'data');
|
|
39
|
-
const knowledgeRoot = path.join(root, 'knowledge');
|
|
40
|
-
const layout = new StorageLayout({ dataDir });
|
|
41
|
-
const settings = new SettingsStore({ layout });
|
|
42
|
-
const registry = new ProjectRegistryStore({ layout });
|
|
43
|
-
const projects = new ProjectStore({ layout });
|
|
44
|
-
await settings.initialize({ knowledge: { rootPath: knowledgeRoot } });
|
|
45
|
-
await registry.initialize();
|
|
46
|
-
const lifecycle = new ProjectLifecycleService({
|
|
47
|
-
layout,
|
|
48
|
-
settingsStore: settings,
|
|
49
|
-
registryStore: registry,
|
|
50
|
-
projectStore: projects,
|
|
51
|
-
triggerScriptPath: TRIGGER,
|
|
52
|
-
bridgeAdapter: { async getHighWatermark() { return { status: 'captured', cursor: 73 }; } },
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
const repo = createRepo(root, 'repo-a');
|
|
56
|
-
const claude = path.join(repo, 'CLAUDE.md');
|
|
57
|
-
fs.writeFileSync(claude, '# user only\n');
|
|
58
|
-
const imported = await lifecycle.importProject({ localPath: repo, projectId: 'project-life-a', storageName: 'repo-a-stable' });
|
|
59
|
-
assert.strictEqual(imported.ok, true);
|
|
60
|
-
assert.strictEqual(imported.config.aiProfileId, null, 'import must not require an AI profile');
|
|
61
|
-
assert.strictEqual(imported.state.trackingStartCommit, git(repo, ['rev-parse', 'HEAD']));
|
|
62
|
-
assert.strictEqual(imported.state.conversationBaselineCursor, 73, 'import must freeze the current Bridge cursor as its conversation baseline');
|
|
63
|
-
assert.deepStrictEqual(fs.readdirSync(imported.config.knowledgePath), [], 'import must not create TODO knowledge files');
|
|
64
|
-
assert.strictEqual(fs.readFileSync(claude, 'utf8'), '# user only\n');
|
|
65
|
-
assert.strictEqual(realHookManager.readHookStatus({ repoPath: repo, projectId: imported.projectId }).installed, true);
|
|
66
|
-
await assert.rejects(lifecycle.importProject({ localPath: repo }), /already imported/);
|
|
67
|
-
|
|
68
|
-
const conflictRepo = createRepo(root, 'repo-conflict');
|
|
69
|
-
const conflictPath = path.join(knowledgeRoot, 'conflict-stable');
|
|
70
|
-
fs.mkdirSync(conflictPath, { recursive: true });
|
|
71
|
-
fs.writeFileSync(path.join(conflictPath, 'user.md'), 'do not overwrite');
|
|
72
|
-
await assert.rejects(lifecycle.importProject({ localPath: conflictRepo, projectId: 'project-conflict', storageName: 'conflict-stable' }), error => error.code === 'MIGRATION_TARGET_CONFLICT');
|
|
73
|
-
assert(!registry.listIds().includes('project-conflict'));
|
|
74
|
-
assert.strictEqual(fs.readFileSync(path.join(conflictPath, 'user.md'), 'utf8'), 'do not overwrite');
|
|
75
|
-
|
|
76
|
-
const failingRepo = createRepo(root, 'repo-failing');
|
|
77
|
-
const failingHooks = { ...realHookManager, installHook() { throw new Error('injected Hook failure'); } };
|
|
78
|
-
const failingLifecycle = new ProjectLifecycleService({ layout, settingsStore: settings, registryStore: registry, projectStore: projects, triggerScriptPath: TRIGGER, hookManager: failingHooks });
|
|
79
|
-
await assert.rejects(failingLifecycle.importProject({ localPath: failingRepo, projectId: 'project-failing', storageName: 'failing-stable' }));
|
|
80
|
-
assert(!registry.listIds().includes('project-failing'));
|
|
81
|
-
assert(!fs.existsSync(layout.getProjectMetadataDir('project-failing')));
|
|
82
|
-
assert(!fs.existsSync(path.join(knowledgeRoot, 'failing-stable')));
|
|
83
|
-
|
|
84
|
-
const emptyRepo = createRepo(root, 'repo-empty', false);
|
|
85
|
-
const empty = await lifecycle.importProject({ localPath: emptyRepo, projectId: 'project-empty', storageName: 'empty-stable' });
|
|
86
|
-
assert.strictEqual(empty.state.trackingMode, 'empty-repo');
|
|
87
|
-
assert.strictEqual(empty.state.trackingStartCommit, null);
|
|
88
|
-
|
|
89
|
-
const failingDelete = new ProjectLifecycleService({
|
|
90
|
-
layout, settingsStore: settings, registryStore: registry, projectStore: projects, triggerScriptPath: TRIGGER,
|
|
91
|
-
hookManager: { ...realHookManager, uninstallHook() { throw new Error('injected uninstall failure'); } },
|
|
92
|
-
});
|
|
93
|
-
await assert.rejects(failingDelete.deleteProject(imported.projectId));
|
|
94
|
-
assert(registry.listIds().includes(imported.projectId), 'Hook uninstall failure must preserve registration');
|
|
95
|
-
|
|
96
|
-
const knowledgePath = imported.config.knowledgePath;
|
|
97
|
-
const deleted = await lifecycle.deleteProject(imported.projectId);
|
|
98
|
-
assert.strictEqual(deleted.removedKnowledge, false);
|
|
99
|
-
assert(fs.existsSync(knowledgePath), 'default delete must preserve external knowledge directory');
|
|
100
|
-
assert(!registry.listIds().includes(imported.projectId));
|
|
101
|
-
|
|
102
|
-
console.log('project-lifecycle-transaction-test PASS');
|
|
103
|
-
})().catch(error => {
|
|
104
|
-
console.error(error.stack || error.message);
|
|
105
|
-
process.exitCode = 1;
|
|
106
|
-
});
|
|
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
|
+
const { StorageLayout } = require('../lib/storage-layout');
|
|
7
|
+
const { SettingsStore } = require('../lib/settings-store');
|
|
8
|
+
const { ProjectRegistryStore } = require('../lib/project-registry-store');
|
|
9
|
+
const { ProjectStore } = require('../lib/project-store');
|
|
10
|
+
const { ProjectLifecycleService } = require('../lib/project-lifecycle-service');
|
|
11
|
+
const realHookManager = require('../lib/hook-manager');
|
|
12
|
+
|
|
13
|
+
const ROOT = path.resolve(__dirname, '..', '..');
|
|
14
|
+
const TRIGGER = path.join(ROOT, '_site', 'scripts', 'hook-trigger.js');
|
|
15
|
+
|
|
16
|
+
function git(repo, args) {
|
|
17
|
+
const result = spawnSync('git', ['-C', repo, ...args], { encoding: 'utf8', windowsHide: true, env: { ...process.env, GIT_TERMINAL_PROMPT: '0' } });
|
|
18
|
+
if (result.status !== 0) throw new Error(result.stderr || `git ${args.join(' ')} failed`);
|
|
19
|
+
return String(result.stdout || '').trim();
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function createRepo(root, name, withCommit = true) {
|
|
23
|
+
const repo = path.join(root, name);
|
|
24
|
+
fs.mkdirSync(repo, { recursive: true });
|
|
25
|
+
git(repo, ['init', '--initial-branch=main']);
|
|
26
|
+
git(repo, ['config', 'user.email', 'life@example.invalid']);
|
|
27
|
+
git(repo, ['config', 'user.name', 'Lifecycle Test']);
|
|
28
|
+
if (withCommit) {
|
|
29
|
+
fs.writeFileSync(path.join(repo, 'README.md'), `# ${name}\n`);
|
|
30
|
+
git(repo, ['add', 'README.md']);
|
|
31
|
+
git(repo, ['commit', '-m', 'initial']);
|
|
32
|
+
}
|
|
33
|
+
return repo;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
(async () => {
|
|
37
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'pk-lifecycle-'));
|
|
38
|
+
const dataDir = path.join(root, 'data');
|
|
39
|
+
const knowledgeRoot = path.join(root, 'knowledge');
|
|
40
|
+
const layout = new StorageLayout({ dataDir });
|
|
41
|
+
const settings = new SettingsStore({ layout });
|
|
42
|
+
const registry = new ProjectRegistryStore({ layout });
|
|
43
|
+
const projects = new ProjectStore({ layout });
|
|
44
|
+
await settings.initialize({ knowledge: { rootPath: knowledgeRoot } });
|
|
45
|
+
await registry.initialize();
|
|
46
|
+
const lifecycle = new ProjectLifecycleService({
|
|
47
|
+
layout,
|
|
48
|
+
settingsStore: settings,
|
|
49
|
+
registryStore: registry,
|
|
50
|
+
projectStore: projects,
|
|
51
|
+
triggerScriptPath: TRIGGER,
|
|
52
|
+
bridgeAdapter: { async getHighWatermark() { return { status: 'captured', cursor: 73 }; } },
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
const repo = createRepo(root, 'repo-a');
|
|
56
|
+
const claude = path.join(repo, 'CLAUDE.md');
|
|
57
|
+
fs.writeFileSync(claude, '# user only\n');
|
|
58
|
+
const imported = await lifecycle.importProject({ localPath: repo, projectId: 'project-life-a', storageName: 'repo-a-stable' });
|
|
59
|
+
assert.strictEqual(imported.ok, true);
|
|
60
|
+
assert.strictEqual(imported.config.aiProfileId, null, 'import must not require an AI profile');
|
|
61
|
+
assert.strictEqual(imported.state.trackingStartCommit, git(repo, ['rev-parse', 'HEAD']));
|
|
62
|
+
assert.strictEqual(imported.state.conversationBaselineCursor, 73, 'import must freeze the current Bridge cursor as its conversation baseline');
|
|
63
|
+
assert.deepStrictEqual(fs.readdirSync(imported.config.knowledgePath), [], 'import must not create TODO knowledge files');
|
|
64
|
+
assert.strictEqual(fs.readFileSync(claude, 'utf8'), '# user only\n');
|
|
65
|
+
assert.strictEqual(realHookManager.readHookStatus({ repoPath: repo, projectId: imported.projectId }).installed, true);
|
|
66
|
+
await assert.rejects(lifecycle.importProject({ localPath: repo }), /already imported/);
|
|
67
|
+
|
|
68
|
+
const conflictRepo = createRepo(root, 'repo-conflict');
|
|
69
|
+
const conflictPath = path.join(knowledgeRoot, 'conflict-stable');
|
|
70
|
+
fs.mkdirSync(conflictPath, { recursive: true });
|
|
71
|
+
fs.writeFileSync(path.join(conflictPath, 'user.md'), 'do not overwrite');
|
|
72
|
+
await assert.rejects(lifecycle.importProject({ localPath: conflictRepo, projectId: 'project-conflict', storageName: 'conflict-stable' }), error => error.code === 'MIGRATION_TARGET_CONFLICT');
|
|
73
|
+
assert(!registry.listIds().includes('project-conflict'));
|
|
74
|
+
assert.strictEqual(fs.readFileSync(path.join(conflictPath, 'user.md'), 'utf8'), 'do not overwrite');
|
|
75
|
+
|
|
76
|
+
const failingRepo = createRepo(root, 'repo-failing');
|
|
77
|
+
const failingHooks = { ...realHookManager, installHook() { throw new Error('injected Hook failure'); } };
|
|
78
|
+
const failingLifecycle = new ProjectLifecycleService({ layout, settingsStore: settings, registryStore: registry, projectStore: projects, triggerScriptPath: TRIGGER, hookManager: failingHooks });
|
|
79
|
+
await assert.rejects(failingLifecycle.importProject({ localPath: failingRepo, projectId: 'project-failing', storageName: 'failing-stable' }));
|
|
80
|
+
assert(!registry.listIds().includes('project-failing'));
|
|
81
|
+
assert(!fs.existsSync(layout.getProjectMetadataDir('project-failing')));
|
|
82
|
+
assert(!fs.existsSync(path.join(knowledgeRoot, 'failing-stable')));
|
|
83
|
+
|
|
84
|
+
const emptyRepo = createRepo(root, 'repo-empty', false);
|
|
85
|
+
const empty = await lifecycle.importProject({ localPath: emptyRepo, projectId: 'project-empty', storageName: 'empty-stable' });
|
|
86
|
+
assert.strictEqual(empty.state.trackingMode, 'empty-repo');
|
|
87
|
+
assert.strictEqual(empty.state.trackingStartCommit, null);
|
|
88
|
+
|
|
89
|
+
const failingDelete = new ProjectLifecycleService({
|
|
90
|
+
layout, settingsStore: settings, registryStore: registry, projectStore: projects, triggerScriptPath: TRIGGER,
|
|
91
|
+
hookManager: { ...realHookManager, uninstallHook() { throw new Error('injected uninstall failure'); } },
|
|
92
|
+
});
|
|
93
|
+
await assert.rejects(failingDelete.deleteProject(imported.projectId));
|
|
94
|
+
assert(registry.listIds().includes(imported.projectId), 'Hook uninstall failure must preserve registration');
|
|
95
|
+
|
|
96
|
+
const knowledgePath = imported.config.knowledgePath;
|
|
97
|
+
const deleted = await lifecycle.deleteProject(imported.projectId);
|
|
98
|
+
assert.strictEqual(deleted.removedKnowledge, false);
|
|
99
|
+
assert(fs.existsSync(knowledgePath), 'default delete must preserve external knowledge directory');
|
|
100
|
+
assert(!registry.listIds().includes(imported.projectId));
|
|
101
|
+
|
|
102
|
+
console.log('project-lifecycle-transaction-test PASS');
|
|
103
|
+
})().catch(error => {
|
|
104
|
+
console.error(error.stack || error.message);
|
|
105
|
+
process.exitCode = 1;
|
|
106
|
+
});
|