@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,151 @@
|
|
|
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 { KnowledgeToolRuntime } = require('../lib/knowledge-tool-runtime');
|
|
12
|
+
const { CommitReconciler } = require('../lib/commit-reconciler');
|
|
13
|
+
const { handlePostCommitEvent } = require('../lib/post-commit-automation');
|
|
14
|
+
const hookManager = require('../lib/hook-manager');
|
|
15
|
+
const { callTool } = require('../../bin/project-knowledge-mcp');
|
|
16
|
+
const { spawnServer } = require('./helpers/spawn-server');
|
|
17
|
+
|
|
18
|
+
const ROOT = path.resolve(__dirname, '..', '..');
|
|
19
|
+
const PORT = 7834;
|
|
20
|
+
const BASE = `http://127.0.0.1:${PORT}`;
|
|
21
|
+
const temp = fs.mkdtempSync(path.join(os.tmpdir(), 'pkb-path-v2-'));
|
|
22
|
+
const dataDir = path.join(temp, 'data');
|
|
23
|
+
const rootA = path.join(temp, 'knowledge-a');
|
|
24
|
+
const rootB = path.join(temp, 'knowledge-b');
|
|
25
|
+
const oldRepo = path.join(temp, 'old-repo');
|
|
26
|
+
const movedRepo = path.join(temp, 'moved-repo');
|
|
27
|
+
const newRepo = path.join(temp, 'new-repo');
|
|
28
|
+
|
|
29
|
+
function git(cwd, args) {
|
|
30
|
+
const result = spawnSync('git', args, { cwd, encoding: 'utf8' });
|
|
31
|
+
assert.strictEqual(result.status, 0, result.stderr);
|
|
32
|
+
return String(result.stdout || '').trim();
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function initializeRepo(cwd, title) {
|
|
36
|
+
fs.mkdirSync(cwd, { recursive: true });
|
|
37
|
+
git(cwd, ['init', '--initial-branch=main']);
|
|
38
|
+
git(cwd, ['config', 'user.email', 'path@example.test']);
|
|
39
|
+
git(cwd, ['config', 'user.name', 'Path Test']);
|
|
40
|
+
fs.writeFileSync(path.join(cwd, 'README.md'), `# ${title}\n`, 'utf8');
|
|
41
|
+
git(cwd, ['add', 'README.md']);
|
|
42
|
+
git(cwd, ['commit', '-m', 'baseline']);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async function waitForServer() {
|
|
46
|
+
for (let attempt = 0; attempt < 100; attempt += 1) {
|
|
47
|
+
try { if ((await fetch(`${BASE}/api/health`)).ok) return; } catch {}
|
|
48
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
49
|
+
}
|
|
50
|
+
throw new Error('server did not start');
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
(async () => {
|
|
54
|
+
let spawned;
|
|
55
|
+
try {
|
|
56
|
+
initializeRepo(oldRepo, 'Old');
|
|
57
|
+
initializeRepo(newRepo, 'New');
|
|
58
|
+
fs.mkdirSync(rootA, { recursive: true });
|
|
59
|
+
fs.mkdirSync(rootB, { recursive: true });
|
|
60
|
+
const oldKnowledge = path.join(rootA, 'old-fixed');
|
|
61
|
+
fs.mkdirSync(path.join(oldKnowledge, 'modules'), { recursive: true });
|
|
62
|
+
fs.writeFileSync(path.join(oldKnowledge, 'modules', 'identity.md'), '# Fixed identity\n\nOld project knowledge remains here.\n', 'utf8');
|
|
63
|
+
|
|
64
|
+
const layout = new StorageLayout({ dataDir });
|
|
65
|
+
const settingsStore = new SettingsStore({ layout });
|
|
66
|
+
const registryStore = new ProjectRegistryStore({ layout });
|
|
67
|
+
const projectStore = new ProjectStore({ layout });
|
|
68
|
+
await settingsStore.initialize({ knowledge: { rootPath: rootA } });
|
|
69
|
+
await registryStore.initialize();
|
|
70
|
+
const baseline = git(oldRepo, ['rev-parse', 'HEAD']);
|
|
71
|
+
const commonDir = git(oldRepo, ['rev-parse', '--path-format=absolute', '--git-common-dir']);
|
|
72
|
+
await projectStore.create('project-stable', {
|
|
73
|
+
displayName: 'Before rename', storageName: 'old-fixed', repoPath: oldRepo, knowledgePath: oldKnowledge,
|
|
74
|
+
repoIdentity: { commonDir }, legacyExtensions: { slug: 'legacy-old' },
|
|
75
|
+
}, { trackingStartCommit: baseline, index: { dirty: true, generation: 1 } });
|
|
76
|
+
await registryStore.add('project-stable', { displayNameSnapshot: 'Before rename' });
|
|
77
|
+
|
|
78
|
+
await settingsStore.updatePatch({ knowledge: { rootPath: rootB } });
|
|
79
|
+
assert.strictEqual(projectStore.readConfig('project-stable').knowledgePath, oldKnowledge, 'global root changes must not move an existing project');
|
|
80
|
+
await projectStore.updateConfig('project-stable', { displayName: 'After rename' });
|
|
81
|
+
assert.strictEqual(projectStore.readConfig('project-stable').projectId, 'project-stable');
|
|
82
|
+
|
|
83
|
+
const lifecycle = new ProjectLifecycleService({ layout, settingsStore, registryStore, projectStore, hookManager, triggerScriptPath: path.join(ROOT, '_site', 'scripts', 'hook-trigger.js') });
|
|
84
|
+
const imported = await lifecycle.importProject({ localPath: newRepo, displayName: 'New root project' });
|
|
85
|
+
assert(layout.isPathInside(rootB, imported.config.knowledgePath, { realpath: true }));
|
|
86
|
+
assert(!layout.isPathInside(rootA, imported.config.knowledgePath, { realpath: true }));
|
|
87
|
+
await settingsStore.updatePatch({ ai: { schema: 'ai-profiles/v1', profiles: [{ id: 'secret', apiKey: 'never-print-this-secret' }] } });
|
|
88
|
+
|
|
89
|
+
fs.renameSync(oldRepo, movedRepo);
|
|
90
|
+
const reconciler = new CommitReconciler({ layout, registryStore, projectStore, requireAiProfile: false });
|
|
91
|
+
reconciler.processCommitEvent = async input => ({ ok: true, projectId: input.projectId, commitSha: input.commitSha, status: 'completed', processed: [] });
|
|
92
|
+
const moved = await handlePostCommitEvent({ schema: 'hook-event/v2', projectId: 'project-stable', repoRoot: movedRepo, head: baseline, branch: 'main' }, { layout, registryStore, projectStore, reconciler });
|
|
93
|
+
assert.strictEqual(moved.ok, true);
|
|
94
|
+
assert.strictEqual(projectStore.readConfig('project-stable').repoPath, path.resolve(movedRepo));
|
|
95
|
+
assert.strictEqual(projectStore.readConfig('project-stable').projectId, 'project-stable');
|
|
96
|
+
|
|
97
|
+
const runtime = new KnowledgeToolRuntime({ layout, settingsStore, registryStore, projectStore, cwd: movedRepo });
|
|
98
|
+
const resolved = runtime.resolveProject({ repoPath: movedRepo });
|
|
99
|
+
assert.strictEqual(resolved.projectId, 'project-stable');
|
|
100
|
+
assert.strictEqual(resolved.knowledgePath, oldKnowledge);
|
|
101
|
+
assert.strictEqual(resolved.indexPath, layout.getIndexPath());
|
|
102
|
+
const mcpResolved = await callTool(runtime, 'project_knowledge_resolve', { projectId: 'project-stable' });
|
|
103
|
+
assert.strictEqual(mcpResolved.knowledgePath, resolved.knowledgePath);
|
|
104
|
+
assert.strictEqual(mcpResolved.indexPath, resolved.indexPath);
|
|
105
|
+
|
|
106
|
+
const protectedFiles = [layout.getSettingsPath(), layout.getProjectRegistryPath(), layout.getProjectConfigPath('project-stable'), layout.getProjectStatePath('project-stable')];
|
|
107
|
+
const beforeRead = new Map(protectedFiles.map(file => [file, fs.readFileSync(file, 'utf8')]));
|
|
108
|
+
const entry = await runtime.get({ projectId: 'project-stable', entry: 'modules/identity.md' });
|
|
109
|
+
assert.match(entry.chunks[0].chunk_text, /Old project knowledge/);
|
|
110
|
+
const search = await runtime.search({ projectId: 'project-stable', query: 'Old project knowledge' });
|
|
111
|
+
assert.strictEqual(search.source, 'knowledge-retrieval-service');
|
|
112
|
+
assert.strictEqual(search.backend, 'markdown-hybrid-fallback');
|
|
113
|
+
const cli = spawnSync(process.execPath, [path.join(ROOT, 'bin', 'project-knowledge-kb.js'), 'get', '--project', 'project-stable', '--entry', 'modules/identity.md', '--json'], {
|
|
114
|
+
cwd: movedRepo, encoding: 'utf8', env: { ...process.env, KB_DATA_DIR: dataDir, KB_SKIP_MIGRATION: '1' },
|
|
115
|
+
});
|
|
116
|
+
assert.strictEqual(cli.status, 0, cli.stderr);
|
|
117
|
+
assert.match(JSON.parse(cli.stdout).chunks[0].chunk_text, /Old project knowledge/);
|
|
118
|
+
const cliError = spawnSync(process.execPath, [path.join(ROOT, 'bin', 'project-knowledge-kb.js'), 'get', '--project', 'missing-project', '--entry', 'README.md', '--json'], {
|
|
119
|
+
cwd: movedRepo, encoding: 'utf8', env: { ...process.env, KB_DATA_DIR: dataDir, KB_SKIP_MIGRATION: '1' },
|
|
120
|
+
});
|
|
121
|
+
assert.notStrictEqual(cliError.status, 0);
|
|
122
|
+
assert(!cliError.stderr.includes('never-print-this-secret'));
|
|
123
|
+
assert(!/\bstack\b/i.test(cliError.stderr));
|
|
124
|
+
for (const file of protectedFiles) assert.strictEqual(fs.readFileSync(file, 'utf8'), beforeRead.get(file), `read query mutated ${file}`);
|
|
125
|
+
await runtime.close();
|
|
126
|
+
|
|
127
|
+
spawned = spawnServer({ root: ROOT, port: PORT, dataDir, tag: 'path-v2' });
|
|
128
|
+
await waitForServer();
|
|
129
|
+
const response = await fetch(`${BASE}/api/projects`);
|
|
130
|
+
const body = await response.json();
|
|
131
|
+
const serverProject = body.projects.find(project => project.projectId === 'project-stable');
|
|
132
|
+
assert.strictEqual(serverProject.config.knowledgePath, oldKnowledge);
|
|
133
|
+
assert.strictEqual(layout.getIndexPath(), path.join(dataDir, 'index', 'knowledge.lancedb'));
|
|
134
|
+
console.log('path-consistency-test PASS');
|
|
135
|
+
} finally {
|
|
136
|
+
if (spawned) {
|
|
137
|
+
try { spawned.child.kill(); } catch {}
|
|
138
|
+
await Promise.race([
|
|
139
|
+
new Promise(resolve => spawned.child.once('exit', resolve)),
|
|
140
|
+
new Promise(resolve => setTimeout(resolve, 2000)),
|
|
141
|
+
]);
|
|
142
|
+
}
|
|
143
|
+
for (let attempt = 0; attempt < 5; attempt += 1) {
|
|
144
|
+
try { fs.rmSync(temp, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 }); break; }
|
|
145
|
+
catch (error) {
|
|
146
|
+
if (attempt === 4) throw error;
|
|
147
|
+
await new Promise(resolve => setTimeout(resolve, 150));
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
})().catch(error => { console.error(error); process.exitCode = 1; });
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
const assert = require('assert');
|
|
2
|
+
const automation = require('../lib/post-commit-automation');
|
|
3
|
+
|
|
4
|
+
// Pending repository scans are not a recovery mechanism. A Hook event is the
|
|
5
|
+
// only authority that can request analysis.
|
|
6
|
+
assert.equal(Object.hasOwn(automation, 'dispatchPendingAutomations'), false);
|
|
7
|
+
assert.equal(Object.hasOwn(automation, 'getQueueSize'), false);
|
|
8
|
+
assert.equal(Object.hasOwn(automation, 'drainQueue'), false);
|
|
9
|
+
console.log('pending-sweep-test PASS');
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
// Run: node _site/_test/post-commit-automation-test.js
|
|
2
|
+
|
|
3
|
+
const assert = require('assert');
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
const automationConfig = require('../lib/automation-config');
|
|
7
|
+
const automation = require('../lib/post-commit-automation');
|
|
8
|
+
const { REQUIREMENT_UNAVAILABLE, renderCommitPrompt } = require('../lib/commit-prompt');
|
|
9
|
+
|
|
10
|
+
(() => {
|
|
11
|
+
const normalized = automationConfig.normalizeAutomationConfig({
|
|
12
|
+
enabled: true,
|
|
13
|
+
hookPromptTemplate: 'Legacy instruction for {{commitHash}}.',
|
|
14
|
+
initPromptTemplate: 'must be ignored',
|
|
15
|
+
postCommitEnabled: true,
|
|
16
|
+
allowReadOnlyBash: true,
|
|
17
|
+
});
|
|
18
|
+
assert.strictEqual(normalized.commitPromptTemplate, 'Legacy instruction for {{commitHash}}.', 'legacy Hook prompt should be a read-only compatibility override');
|
|
19
|
+
assert.strictEqual(normalized.legacyPromptOverride, true);
|
|
20
|
+
assert(!Object.hasOwn(normalized, 'hookPromptTemplate'));
|
|
21
|
+
assert(!Object.hasOwn(normalized, 'initPromptTemplate'));
|
|
22
|
+
assert(!Object.hasOwn(normalized, 'postCommitEnabled'));
|
|
23
|
+
assert(!Object.hasOwn(normalized, 'allowReadOnlyBash'));
|
|
24
|
+
assert(!Object.hasOwn(automationConfig, 'DEFAULT_INIT_PROMPT_TEMPLATE'), 'init prompt constant must not be exported');
|
|
25
|
+
assert(!Object.hasOwn(automation, 'dispatchProjectInit'), 'project init dispatch must not be public');
|
|
26
|
+
assert(!Object.hasOwn(automation, 'dispatchAutomation'), 'generic automation dispatch must not be public');
|
|
27
|
+
|
|
28
|
+
const staging = path.resolve('runtime', 'staging', 'run-test');
|
|
29
|
+
const evidenceRoot = path.resolve('runtime', 'runs', 'run-test', 'input', 'evidence');
|
|
30
|
+
const policy = automationConfig.buildAutomationToolPolicy({ stagingPath: staging, evidenceRoot });
|
|
31
|
+
assert.strictEqual(policy.canWriteKb, false, 'AI must never write final knowledge directly');
|
|
32
|
+
assert.strictEqual(policy.canWriteStaging, true);
|
|
33
|
+
assert(!policy.allowedTools.includes('Bash'), 'generic Bash must not be available to analysis');
|
|
34
|
+
assert.strictEqual(automationConfig.evaluateAutomationToolUse(policy, 'Write', { file_path: path.join(staging, 'manifest.json') }).behavior, 'allow');
|
|
35
|
+
assert.strictEqual(automationConfig.evaluateAutomationToolUse(policy, 'Write', { file_path: path.resolve('source.js') }).behavior, 'deny');
|
|
36
|
+
assert.strictEqual(automationConfig.evaluateAutomationToolUse(policy, 'Read', { file_path: path.join(evidenceRoot, 'patch-manifest.json') }).behavior, 'allow');
|
|
37
|
+
assert.strictEqual(automationConfig.evaluateAutomationToolUse(policy, 'Write', { file_path: path.join(evidenceRoot, 'patches', '000001.patch') }).behavior, 'deny');
|
|
38
|
+
assert.strictEqual(automationConfig.evaluateAutomationToolUse(policy, 'Read', { file_path: path.resolve('repo', 'source.js') }).behavior, 'deny');
|
|
39
|
+
assert.strictEqual(automationConfig.evaluateAutomationToolUse(policy, 'Bash', { command: 'git show' }).behavior, 'deny');
|
|
40
|
+
|
|
41
|
+
const evidence = {
|
|
42
|
+
commitSha: 'a'.repeat(40),
|
|
43
|
+
parents: ['b'.repeat(40)],
|
|
44
|
+
author: 'Test Author',
|
|
45
|
+
date: '2026-08-17T00:00:00Z',
|
|
46
|
+
subject: 'feat: exact evidence',
|
|
47
|
+
branch: 'main',
|
|
48
|
+
patchMode: 'parent',
|
|
49
|
+
patchHash: 'sha256:patch',
|
|
50
|
+
patchBytes: 42,
|
|
51
|
+
patchOmitted: false,
|
|
52
|
+
patch: 'diff --git a/a.js b/a.js\n+implemented\n',
|
|
53
|
+
files: [{ status: 'M', path: 'a.js', oldPath: null, binary: false, added: 1, deleted: 0 }],
|
|
54
|
+
};
|
|
55
|
+
const config = {
|
|
56
|
+
projectId: 'project-test',
|
|
57
|
+
displayName: 'Prompt Test',
|
|
58
|
+
repoPath: path.resolve('repo'),
|
|
59
|
+
knowledgePath: path.resolve('knowledge'),
|
|
60
|
+
automation: { commitPromptTemplate: automationConfig.DEFAULT_COMMIT_PROMPT_TEMPLATE },
|
|
61
|
+
};
|
|
62
|
+
const missing = renderCommitPrompt({ config, evidence, requirements: [], existingKnowledge: { entries: [], omitted: [] } });
|
|
63
|
+
assert(missing.prompt.includes(REQUIREMENT_UNAVAILABLE), 'unavailable requirement must use the fixed non-guessing text');
|
|
64
|
+
assert(missing.prompt.includes(evidence.patch), 'prompt must contain the actual patch');
|
|
65
|
+
assert(missing.prompt.includes('现有相关知识'), 'prompt must contain existing knowledge section');
|
|
66
|
+
assert(missing.prompt.includes('新增、更新或删除'), 'prompt must ask for add/update/delete decisions');
|
|
67
|
+
assert(!missing.prompt.includes('git-hook') && !missing.prompt.includes('startup'), 'trigger must not affect prompt content');
|
|
68
|
+
|
|
69
|
+
const requirement = { id: 'req-1', client: 'codex', sessionId: 'session-1', ts: '2026-08-17T00:00:00Z', requirement: 'Implement the exact feature.' };
|
|
70
|
+
const fromHook = renderCommitPrompt({ config, evidence, requirements: [requirement], existingKnowledge: { entries: [] } });
|
|
71
|
+
const fromStartup = renderCommitPrompt({ config, evidence, requirements: [requirement], existingKnowledge: { entries: [] } });
|
|
72
|
+
assert.strictEqual(fromHook.promptHash, fromStartup.promptHash, 'Hook and startup must render the identical prompt hash');
|
|
73
|
+
assert(fromHook.prompt.includes('Implement the exact feature.'), 'recorded user requirement must be present');
|
|
74
|
+
|
|
75
|
+
const legacy = renderCommitPrompt({
|
|
76
|
+
config: { ...config, automation: { hookPromptTemplate: 'Legacy {{commitHash}}' } },
|
|
77
|
+
evidence,
|
|
78
|
+
requirements: [],
|
|
79
|
+
existingKnowledge: { entries: [] },
|
|
80
|
+
});
|
|
81
|
+
assert(legacy.prompt.includes('Legacy ' + evidence.commitSha), 'legacy Hook prompt override should still render');
|
|
82
|
+
assert(legacy.prompt.includes(REQUIREMENT_UNAVAILABLE) && legacy.prompt.includes(evidence.patch), 'mandatory evidence sections must be appended to incomplete legacy templates');
|
|
83
|
+
|
|
84
|
+
const source = fs.readFileSync(path.join(__dirname, '..', 'lib', 'post-commit-automation.js'), 'utf8');
|
|
85
|
+
assert(!/dispatchProjectInit|renderProjectInitPrompt|project-init|startup-recovery/.test(source), 'automation source must not retain init or a third trigger name');
|
|
86
|
+
console.log('post-commit-automation-test PASS');
|
|
87
|
+
})();
|
|
@@ -0,0 +1,55 @@
|
|
|
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 { SettingsStore } = require('../lib/settings-store');
|
|
7
|
+
const { ProjectRegistryStore } = require('../lib/project-registry-store');
|
|
8
|
+
const { ProjectStore } = require('../lib/project-store');
|
|
9
|
+
const { KnowledgeToolRuntime } = require('../lib/knowledge-tool-runtime');
|
|
10
|
+
const { validateKb, buildPrContextPack } = require('../lib/kb-validator');
|
|
11
|
+
const { regenerateIndexes } = require('../lib/index-builder');
|
|
12
|
+
|
|
13
|
+
const temp = fs.mkdtempSync(path.join(os.tmpdir(), 'pkb-pr-v2-'));
|
|
14
|
+
const knowledgePath = path.join(temp, 'knowledge');
|
|
15
|
+
|
|
16
|
+
function writeKnowledge() {
|
|
17
|
+
fs.mkdirSync(path.join(knowledgePath, 'modules'), { recursive: true });
|
|
18
|
+
fs.mkdirSync(path.join(knowledgePath, 'changes'), { recursive: true });
|
|
19
|
+
fs.writeFileSync(path.join(knowledgePath, 'README.md'), '# Test KB\n');
|
|
20
|
+
fs.writeFileSync(path.join(knowledgePath, 'GOAL.md'), '# Goal\n');
|
|
21
|
+
fs.writeFileSync(path.join(knowledgePath, 'ARCHITECTURE.md'), '# Architecture\n');
|
|
22
|
+
fs.writeFileSync(path.join(knowledgePath, 'modules', 'api.md'), '---\ntags: [api]\nsourcePaths: [src/api.js]\n---\n# API\n');
|
|
23
|
+
fs.writeFileSync(path.join(knowledgePath, 'changes', 'api-change.md'), '---\ntags: [api]\naffectedModules: [api]\ndevelopmentIntent: Add API memory.\n---\n# API change\n\n## Development Intent\nAdd API memory.\n\n## Implementation Result\nDone.\n\n## Evidence\n- src/api.js\n');
|
|
24
|
+
regenerateIndexes(knowledgePath);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
(async () => {
|
|
28
|
+
try {
|
|
29
|
+
writeKnowledge();
|
|
30
|
+
const validation = validateKb(knowledgePath);
|
|
31
|
+
assert(validation.ok, JSON.stringify(validation.errors));
|
|
32
|
+
const pack = buildPrContextPack(knowledgePath);
|
|
33
|
+
assert(pack.ok);
|
|
34
|
+
assert.strictEqual(pack.pack.schema, 'pr-context-pack/v1');
|
|
35
|
+
assert(pack.pack.trustedKnowledge.some(item => item.path === 'modules/api.md'));
|
|
36
|
+
assert(!JSON.stringify(pack.pack).includes('_ai/'));
|
|
37
|
+
|
|
38
|
+
const layout = new StorageLayout({ dataDir: path.join(temp, 'data') });
|
|
39
|
+
const settingsStore = new SettingsStore({ layout });
|
|
40
|
+
const registryStore = new ProjectRegistryStore({ layout });
|
|
41
|
+
const projectStore = new ProjectStore({ layout });
|
|
42
|
+
await settingsStore.initialize({ knowledge: { rootPath: path.join(temp, 'future') } });
|
|
43
|
+
await registryStore.initialize();
|
|
44
|
+
await projectStore.create('project-consumer', { displayName: 'Consumer', storageName: 'consumer', repoPath: temp, knowledgePath }, { index: { dirty: true, generation: 1 } });
|
|
45
|
+
await registryStore.add('project-consumer', { displayNameSnapshot: 'Consumer' });
|
|
46
|
+
const runtime = new KnowledgeToolRuntime({ layout, settingsStore, registryStore, projectStore, cwd: temp });
|
|
47
|
+
const entry = await runtime.get({ projectId: 'project-consumer', entry: 'modules/api.md' });
|
|
48
|
+
assert.match(entry.chunks[0].chunk_text, /# API/);
|
|
49
|
+
await assert.rejects(runtime.get({ projectId: 'project-consumer', entry: '../settings.json' }), error => error.code === 'PATH_OUTSIDE_ROOT');
|
|
50
|
+
await runtime.close();
|
|
51
|
+
console.log('pr-consumer-contract-test PASS');
|
|
52
|
+
} finally {
|
|
53
|
+
fs.rmSync(temp, { recursive: true, force: true });
|
|
54
|
+
}
|
|
55
|
+
})().catch(error => { console.error(error); process.exitCode = 1; });
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
const assert = require('assert');
|
|
2
|
+
const os = require('os');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const { spawnServer } = require('./helpers/spawn-server');
|
|
5
|
+
const { findChrome } = require('./helpers/find-chrome');
|
|
6
|
+
const { launchCdpBrowser, requestJson, waitFor } = require('./helpers/cdp-browser');
|
|
7
|
+
const { createLoggingUiFixture } = require('./helpers/logging-ui-fixture');
|
|
8
|
+
|
|
9
|
+
const ROOT = path.resolve(__dirname, '..', '..');
|
|
10
|
+
const sitePort = 8500 + (process.pid % 200);
|
|
11
|
+
const profileDir = path.join(os.tmpdir(), 'pk-project-status-profile-' + process.pid);
|
|
12
|
+
|
|
13
|
+
(async () => {
|
|
14
|
+
const fixture = await createLoggingUiFixture({ totalLogs: 8 });
|
|
15
|
+
const server = spawnServer({ root: ROOT, port: sitePort, dataDir: fixture.dataDir, tag: 'project-status-v13', stdio: 'ignore' });
|
|
16
|
+
let browser;
|
|
17
|
+
try {
|
|
18
|
+
await waitFor(() => requestJson(`http://127.0.0.1:${sitePort}/api/projects`).then(body => body.projects.length === 1), 'project API', 20000);
|
|
19
|
+
browser = await launchCdpBrowser({ chrome: findChrome(), profileDir, url: `http://127.0.0.1:${sitePort}/`, width: 1200, height: 860 });
|
|
20
|
+
await waitFor(() => browser.evaluate('window.__PK_APP__ && window.__PK_APP__.getState().projects === 1'), 'project shell');
|
|
21
|
+
|
|
22
|
+
const shell = await browser.evaluate(`(() => ({
|
|
23
|
+
projectButtons: document.querySelectorAll('#project-list .project-card').length,
|
|
24
|
+
selected: document.querySelector('#project-list .project-card.active')?.dataset.projectId,
|
|
25
|
+
name: document.querySelector('#project-list .project-card.active .project-name')?.textContent,
|
|
26
|
+
badges: document.querySelectorAll('#project-list .mbadge').length,
|
|
27
|
+
path: document.querySelector('#project-list .project-card.active .project-path')?.textContent,
|
|
28
|
+
terminalView: document.getElementById('view-terminal').classList.contains('active'),
|
|
29
|
+
manualControls: document.querySelectorAll('[data-install-hook], [data-uninstall-hook], [data-analyze], [data-simulate]').length,
|
|
30
|
+
mainConversationLinks: [...document.querySelectorAll('.sidebar-nav .nav-btn, .mobile-strip button')].filter(node => /开发对话|运行记录|系统日志/.test(node.textContent)).length
|
|
31
|
+
}))()`);
|
|
32
|
+
assert.strictEqual(shell.projectButtons, 1);
|
|
33
|
+
assert.strictEqual(shell.selected, fixture.projectId);
|
|
34
|
+
assert.strictEqual(shell.name, '视觉检测知识库');
|
|
35
|
+
// 三个模块徽章:端(Agent 终端)、检(vector-hub)、桥(会话浏览)
|
|
36
|
+
assert.strictEqual(shell.badges, 3);
|
|
37
|
+
assert(shell.path.includes(fixture.repo.path));
|
|
38
|
+
assert.strictEqual(shell.terminalView, true);
|
|
39
|
+
assert.strictEqual(shell.manualControls, 0);
|
|
40
|
+
assert.strictEqual(shell.mainConversationLinks, 0);
|
|
41
|
+
|
|
42
|
+
// 删除入口:右键项目卡 → 管理菜单 → 移除项目(原"知识库存储"设置节已按终版移除)
|
|
43
|
+
await browser.evaluate(`(() => {
|
|
44
|
+
const card = document.querySelector('#project-list .project-card');
|
|
45
|
+
card.dispatchEvent(new MouseEvent('contextmenu', { bubbles: true, cancelable: true, clientX: 300, clientY: 200 }));
|
|
46
|
+
})()`);
|
|
47
|
+
await waitFor(() => browser.evaluate("document.getElementById('context-menu').classList.contains('show')"), 'context menu');
|
|
48
|
+
await browser.evaluate("document.getElementById('remove-project').click()");
|
|
49
|
+
await waitFor(() => browser.evaluate("document.getElementById('delete-dialog').open"), 'delete modal');
|
|
50
|
+
assert((await browser.evaluate("document.getElementById('delete-copy').textContent")).includes('视觉检测知识库'));
|
|
51
|
+
await browser.evaluate("document.getElementById('delete-knowledge').click()");
|
|
52
|
+
assert.strictEqual(await browser.evaluate("document.getElementById('delete-confirm-field').hidden"), false, 'knowledge deletion needs explicit project-id confirmation');
|
|
53
|
+
assert.strictEqual(browser.exceptions.length, 0, 'browser runtime exceptions: ' + JSON.stringify(browser.exceptions));
|
|
54
|
+
console.log('project control panel task14 test PASS');
|
|
55
|
+
} finally {
|
|
56
|
+
if (browser) await browser.close();
|
|
57
|
+
await server.cleanup();
|
|
58
|
+
fixture.cleanup();
|
|
59
|
+
}
|
|
60
|
+
})().catch(error => {
|
|
61
|
+
console.error(error.stack || error.message);
|
|
62
|
+
process.exitCode = 1;
|
|
63
|
+
});
|
|
@@ -0,0 +1,64 @@
|
|
|
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 { ProjectRegistryStore } = require('../lib/project-registry-store');
|
|
7
|
+
const { ProjectStore } = require('../lib/project-store');
|
|
8
|
+
const { ProjectLifecycleService } = require('../lib/project-lifecycle-service');
|
|
9
|
+
|
|
10
|
+
(async () => {
|
|
11
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'pk-delete-recovery-'));
|
|
12
|
+
const layout = new StorageLayout({ dataDir: path.join(root, 'data') });
|
|
13
|
+
const registry = new ProjectRegistryStore({ layout });
|
|
14
|
+
const projects = new ProjectStore({ layout });
|
|
15
|
+
await registry.initialize();
|
|
16
|
+
const faultPoints = [
|
|
17
|
+
'delete-tombstoned',
|
|
18
|
+
'delete-before-hook', 'delete-after-hook',
|
|
19
|
+
'delete-before-registry', 'delete-after-registry',
|
|
20
|
+
'delete-before-metadata', 'delete-after-metadata',
|
|
21
|
+
'delete-before-knowledge', 'delete-after-knowledge',
|
|
22
|
+
'delete-before-completed',
|
|
23
|
+
];
|
|
24
|
+
const hooks = {
|
|
25
|
+
uninstallHook({ projectId }) { return { ok: true, removed: true, projectId }; },
|
|
26
|
+
};
|
|
27
|
+
for (let index = 0; index < faultPoints.length; index += 1) {
|
|
28
|
+
const projectId = `project-delete-${String(index).padStart(2, '0')}`;
|
|
29
|
+
const repoPath = path.join(root, 'repos', projectId);
|
|
30
|
+
const knowledgePath = path.join(root, 'knowledge', projectId);
|
|
31
|
+
fs.mkdirSync(repoPath, { recursive: true });
|
|
32
|
+
fs.mkdirSync(knowledgePath, { recursive: true });
|
|
33
|
+
fs.writeFileSync(path.join(knowledgePath, 'README.md'), '# User knowledge\n', 'utf8');
|
|
34
|
+
await projects.create(projectId, { displayName: projectId, storageName: projectId, repoPath, knowledgePath });
|
|
35
|
+
await registry.add(projectId, { displayNameSnapshot: projectId });
|
|
36
|
+
let injected = false;
|
|
37
|
+
const failing = new ProjectLifecycleService({
|
|
38
|
+
layout, registryStore: registry, projectStore: projects, hookManager: hooks,
|
|
39
|
+
fault(point) {
|
|
40
|
+
if (!injected && point === faultPoints[index]) { injected = true; throw new Error(`fault:${point}`); }
|
|
41
|
+
},
|
|
42
|
+
});
|
|
43
|
+
await assert.rejects(
|
|
44
|
+
failing.deleteProject(projectId, { deleteKnowledge: true, confirmationToken: projectId }),
|
|
45
|
+
error => /fault:|can be retried/.test(error.message),
|
|
46
|
+
`fault ${faultPoints[index]} must stop the first attempt`,
|
|
47
|
+
);
|
|
48
|
+
assert(fs.existsSync(failing.deleteTransactionPath(projectId)), 'a durable tombstone must exist before any externally visible mutation');
|
|
49
|
+
|
|
50
|
+
const recovery = new ProjectLifecycleService({ layout, registryStore: registry, projectStore: projects, hookManager: hooks });
|
|
51
|
+
const recovered = await recovery.deleteProject(projectId, { deleteKnowledge: true, confirmationToken: projectId });
|
|
52
|
+
assert.strictEqual(recovered.ok, true, `fault ${faultPoints[index]} must recover on retry`);
|
|
53
|
+
assert(!registry.listIds().includes(projectId));
|
|
54
|
+
assert(!fs.existsSync(layout.getProjectMetadataDir(projectId)));
|
|
55
|
+
assert(!fs.existsSync(knowledgePath));
|
|
56
|
+
const journal = recovery.readDeleteJournal(projectId);
|
|
57
|
+
assert.strictEqual(journal.phase, 'completed');
|
|
58
|
+
assert(Object.values(journal.steps).every(step => step.completed), 'every delete step must be durably terminal');
|
|
59
|
+
}
|
|
60
|
+
console.log('project-delete-recovery-test PASS');
|
|
61
|
+
})().catch(error => {
|
|
62
|
+
console.error(error.stack || error.message);
|
|
63
|
+
process.exitCode = 1;
|
|
64
|
+
});
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
// _site/_test/project-goal-editor-test.js
|
|
2
|
+
//
|
|
3
|
+
// T13: Project Goal editor.
|
|
4
|
+
// GET /api/projects/:id/goal -> read GOAL.md from project knowledge path
|
|
5
|
+
// PUT /api/projects/:id/goal -> atomic write GOAL.md
|
|
6
|
+
// Verifies:
|
|
7
|
+
// - missing GOAL.md returns exists=false;
|
|
8
|
+
// - PUT creates the file atomically;
|
|
9
|
+
// - subsequent GET returns the persisted content;
|
|
10
|
+
// - empty content allowed (no auto-template);
|
|
11
|
+
// - the file is scoped to the project's own knowledgePath (no
|
|
12
|
+
// cross-project writes);
|
|
13
|
+
// - 413 when content exceeds 256KB.
|
|
14
|
+
|
|
15
|
+
const assert = require('assert');
|
|
16
|
+
const fs = require('fs');
|
|
17
|
+
const os = require('os');
|
|
18
|
+
const path = require('path');
|
|
19
|
+
const { spawnSync } = require('child_process');
|
|
20
|
+
const { spawnServer } = require('./helpers/spawn-server');
|
|
21
|
+
|
|
22
|
+
const ROOT = path.resolve(__dirname, '..', '..');
|
|
23
|
+
const sitePort = 8000 + (process.pid % 200);
|
|
24
|
+
const dataDir = fs.mkdtempSync(path.join(os.tmpdir(), 'pk-goal-' + process.pid + '-'));
|
|
25
|
+
const repo = fs.mkdtempSync(path.join(os.tmpdir(), 'pk-goal-repo-' + process.pid + '-'));
|
|
26
|
+
|
|
27
|
+
function git(args) {
|
|
28
|
+
const result = spawnSync('git', ['-C', repo, ...args], {
|
|
29
|
+
encoding: 'utf8', windowsHide: true,
|
|
30
|
+
env: { ...process.env, GIT_TERMINAL_PROMPT: '0' },
|
|
31
|
+
});
|
|
32
|
+
if (result.status !== 0) throw new Error(result.stderr || `git ${args.join(' ')} failed: ${result.status}`);
|
|
33
|
+
return String(result.stdout || '').trim();
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async function serverFetch(url, init, timeoutMs = 10000) {
|
|
37
|
+
const controller = new AbortController();
|
|
38
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
39
|
+
try {
|
|
40
|
+
const response = await fetch(url, { ...init, signal: controller.signal });
|
|
41
|
+
const text = await response.text();
|
|
42
|
+
return { status: response.status, body: text ? JSON.parse(text) : {} };
|
|
43
|
+
} finally {
|
|
44
|
+
clearTimeout(timer);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
(async () => {
|
|
49
|
+
git(['init', '-q', '-b', 'main']);
|
|
50
|
+
git(['config', 'user.email', 'goal@example.local']);
|
|
51
|
+
git(['config', 'user.name', 'Goal']);
|
|
52
|
+
fs.writeFileSync(path.join(repo, 'README.md'), '# goal\n');
|
|
53
|
+
git(['add', 'README.md']);
|
|
54
|
+
git(['commit', '-q', '-m', 'baseline']);
|
|
55
|
+
const knowledgeRoot = path.join(dataDir, 'knowledge');
|
|
56
|
+
fs.mkdirSync(knowledgeRoot, { recursive: true });
|
|
57
|
+
const server = spawnServer({
|
|
58
|
+
root: ROOT, port: sitePort, dataDir, tag: 'goal',
|
|
59
|
+
extraEnv: { KB_AUTOMATION_FAKE_CLAUDE: '1', KB_EMBEDDING_FAKE: '1', KB_SKIP_MIGRATION: '1' },
|
|
60
|
+
});
|
|
61
|
+
try {
|
|
62
|
+
for (let attempt = 0; attempt < 100; attempt += 1) {
|
|
63
|
+
try { const r = await serverFetch(`http://127.0.0.1:${sitePort}/api/health`); if (r.body.ok) break; } catch {}
|
|
64
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
65
|
+
}
|
|
66
|
+
await serverFetch(`http://127.0.0.1:${sitePort}/api/settings`, {
|
|
67
|
+
method: 'PATCH', headers: { 'content-type': 'application/json' },
|
|
68
|
+
body: JSON.stringify({ knowledge: { rootPath: knowledgeRoot } }),
|
|
69
|
+
});
|
|
70
|
+
await serverFetch(`http://127.0.0.1:${sitePort}/api/ai-profiles`, {
|
|
71
|
+
method: 'PUT', headers: { 'content-type': 'application/json' },
|
|
72
|
+
body: JSON.stringify({
|
|
73
|
+
schema: 'ai-profiles/v1', defaultProfileId: 'fake',
|
|
74
|
+
profiles: [{ id: 'fake', name: 'Fake', enabled: true, vendor: 'anthropic', model: 'fake', apiKeyUpdate: { mode: 'replace', value: 'k' } }],
|
|
75
|
+
}),
|
|
76
|
+
});
|
|
77
|
+
const importR = await serverFetch(`http://127.0.0.1:${sitePort}/api/projects/import`, {
|
|
78
|
+
method: 'POST', headers: { 'content-type': 'application/json' },
|
|
79
|
+
body: JSON.stringify({ localPath: repo, aiProfileId: 'fake' }),
|
|
80
|
+
});
|
|
81
|
+
assert.strictEqual(importR.status, 201, JSON.stringify(importR.body));
|
|
82
|
+
const projectId = importR.body.projectId;
|
|
83
|
+
const knowledgePath = importR.body.config.knowledgePath;
|
|
84
|
+
|
|
85
|
+
// Case 1: GET on missing GOAL.md returns exists=false
|
|
86
|
+
let r = await serverFetch(`http://127.0.0.1:${sitePort}/api/projects/${projectId}/goal`, undefined);
|
|
87
|
+
assert.strictEqual(r.status, 200);
|
|
88
|
+
assert.strictEqual(r.body.exists, false);
|
|
89
|
+
assert.strictEqual(r.body.content, '');
|
|
90
|
+
|
|
91
|
+
// Case 2: PUT creates the file atomically with the supplied content
|
|
92
|
+
r = await serverFetch(`http://127.0.0.1:${sitePort}/api/projects/${projectId}/goal`, {
|
|
93
|
+
method: 'PUT', headers: { 'content-type': 'application/json' },
|
|
94
|
+
body: JSON.stringify({ content: '# Project Goal\n\nBuild a knowledge base for v4.1.22 -> main recovery.\n' }),
|
|
95
|
+
});
|
|
96
|
+
assert.strictEqual(r.status, 200, JSON.stringify(r.body));
|
|
97
|
+
const goalPath = path.join(knowledgePath, 'GOAL.md');
|
|
98
|
+
assert(fs.existsSync(goalPath), 'GOAL.md must be created');
|
|
99
|
+
assert(fs.readFileSync(goalPath, 'utf8').includes('v4.1.22'), 'goal content must include the user-supplied text');
|
|
100
|
+
|
|
101
|
+
// Case 3: GET now returns the persisted content
|
|
102
|
+
r = await serverFetch(`http://127.0.0.1:${sitePort}/api/projects/${projectId}/goal`, undefined);
|
|
103
|
+
assert.strictEqual(r.status, 200);
|
|
104
|
+
assert.strictEqual(r.body.exists, true);
|
|
105
|
+
assert(r.body.content.includes('v4.1.22'));
|
|
106
|
+
|
|
107
|
+
// Case 4: empty content is allowed (no auto-template)
|
|
108
|
+
r = await serverFetch(`http://127.0.0.1:${sitePort}/api/projects/${projectId}/goal`, {
|
|
109
|
+
method: 'PUT', headers: { 'content-type': 'application/json' },
|
|
110
|
+
body: JSON.stringify({ content: '' }),
|
|
111
|
+
});
|
|
112
|
+
assert.strictEqual(r.status, 200);
|
|
113
|
+
assert.strictEqual(fs.readFileSync(goalPath, 'utf8'), '', 'empty content must clear the file');
|
|
114
|
+
|
|
115
|
+
// Case 5: writes are scoped to the project's own knowledgePath
|
|
116
|
+
r = await serverFetch(`http://127.0.0.1:${sitePort}/api/projects/${projectId}/goal`, {
|
|
117
|
+
method: 'PUT', headers: { 'content-type': 'application/json' },
|
|
118
|
+
body: JSON.stringify({ content: 'real content\n' }),
|
|
119
|
+
});
|
|
120
|
+
assert.strictEqual(r.status, 200);
|
|
121
|
+
const otherFiles = fs.readdirSync(knowledgePath).filter(name => name !== 'GOAL.md');
|
|
122
|
+
assert.deepStrictEqual(otherFiles, [], 'only GOAL.md must be created in the project knowledge path');
|
|
123
|
+
|
|
124
|
+
// Case 6: 413 when content exceeds 256KB
|
|
125
|
+
const huge = 'x'.repeat(257 * 1024);
|
|
126
|
+
r = await serverFetch(`http://127.0.0.1:${sitePort}/api/projects/${projectId}/goal`, {
|
|
127
|
+
method: 'PUT', headers: { 'content-type': 'application/json' },
|
|
128
|
+
body: JSON.stringify({ content: huge }),
|
|
129
|
+
});
|
|
130
|
+
assert.strictEqual(r.status, 413);
|
|
131
|
+
|
|
132
|
+
// Case 7: GET on a non-existent project returns 404
|
|
133
|
+
r = await serverFetch(`http://127.0.0.1:${sitePort}/api/projects/does-not-exist/goal`, undefined);
|
|
134
|
+
assert.strictEqual(r.status, 404);
|
|
135
|
+
} finally {
|
|
136
|
+
server.child.kill();
|
|
137
|
+
await new Promise(resolve => server.child.once('exit', resolve));
|
|
138
|
+
fs.rmSync(dataDir, { recursive: true, force: true });
|
|
139
|
+
fs.rmSync(repo, { recursive: true, force: true });
|
|
140
|
+
}
|
|
141
|
+
console.log('project-goal-editor-test PASS');
|
|
142
|
+
})().catch(error => {
|
|
143
|
+
console.error(error.stack || error.message);
|
|
144
|
+
process.exit(1);
|
|
145
|
+
});
|