tlc-claude-code 2.0.1 → 2.2.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/.claude/agents/builder.md +144 -0
- package/.claude/agents/planner.md +143 -0
- package/.claude/agents/reviewer.md +160 -0
- package/.claude/commands/tlc/build.md +4 -0
- package/.claude/commands/tlc/deploy.md +194 -2
- package/.claude/commands/tlc/e2e-verify.md +214 -0
- package/.claude/commands/tlc/guard.md +191 -0
- package/.claude/commands/tlc/help.md +32 -0
- package/.claude/commands/tlc/init.md +73 -37
- package/.claude/commands/tlc/llm.md +19 -4
- package/.claude/commands/tlc/preflight.md +134 -0
- package/.claude/commands/tlc/review-plan.md +363 -0
- package/.claude/commands/tlc/review.md +172 -57
- package/.claude/commands/tlc/watchci.md +159 -0
- package/.claude/hooks/tlc-block-tools.sh +41 -0
- package/.claude/hooks/tlc-capture-exchange.sh +50 -0
- package/.claude/hooks/tlc-post-build.sh +38 -0
- package/.claude/hooks/tlc-post-push.sh +22 -0
- package/.claude/hooks/tlc-prompt-guard.sh +69 -0
- package/.claude/hooks/tlc-session-init.sh +123 -0
- package/CLAUDE.md +13 -0
- package/bin/install.js +268 -2
- package/bin/postinstall.js +102 -24
- package/bin/setup-autoupdate.js +206 -0
- package/bin/setup-autoupdate.test.js +124 -0
- package/bin/tlc.js +0 -0
- package/dashboard-web/dist/assets/index-CdS5CHqu.css +1 -0
- package/dashboard-web/dist/assets/index-CwNPPVpg.js +483 -0
- package/dashboard-web/dist/assets/index-CwNPPVpg.js.map +1 -0
- package/dashboard-web/dist/index.html +2 -2
- package/docker-compose.dev.yml +18 -12
- package/package.json +4 -2
- package/scripts/project-docs.js +1 -1
- package/server/index.js +228 -2
- package/server/lib/capture-bridge.js +242 -0
- package/server/lib/capture-bridge.test.js +363 -0
- package/server/lib/capture-guard.js +140 -0
- package/server/lib/capture-guard.test.js +182 -0
- package/server/lib/command-runner.js +159 -0
- package/server/lib/command-runner.test.js +92 -0
- package/server/lib/cost-tracker.test.js +49 -12
- package/server/lib/deploy/runners/dependency-runner.js +106 -0
- package/server/lib/deploy/runners/dependency-runner.test.js +148 -0
- package/server/lib/deploy/runners/secrets-runner.js +174 -0
- package/server/lib/deploy/runners/secrets-runner.test.js +127 -0
- package/server/lib/deploy/security-gates.js +11 -24
- package/server/lib/deploy/security-gates.test.js +9 -2
- package/server/lib/deploy-engine.js +182 -0
- package/server/lib/deploy-engine.test.js +147 -0
- package/server/lib/docker-api.js +137 -0
- package/server/lib/docker-api.test.js +202 -0
- package/server/lib/docker-client.js +297 -0
- package/server/lib/docker-client.test.js +308 -0
- package/server/lib/input-sanitizer.js +86 -0
- package/server/lib/input-sanitizer.test.js +117 -0
- package/server/lib/launchd-agent.js +225 -0
- package/server/lib/launchd-agent.test.js +185 -0
- package/server/lib/memory-api.js +3 -1
- package/server/lib/memory-api.test.js +3 -5
- package/server/lib/memory-bridge-e2e.test.js +160 -0
- package/server/lib/memory-committer.js +18 -4
- package/server/lib/memory-committer.test.js +21 -0
- package/server/lib/memory-hooks-capture.test.js +69 -4
- package/server/lib/memory-hooks-integration.test.js +98 -0
- package/server/lib/memory-hooks.js +42 -4
- package/server/lib/memory-store-adapter.js +105 -0
- package/server/lib/memory-store-adapter.test.js +141 -0
- package/server/lib/memory-wiring-e2e.test.js +93 -0
- package/server/lib/nginx-config.js +114 -0
- package/server/lib/nginx-config.test.js +82 -0
- package/server/lib/ollama-health.js +91 -0
- package/server/lib/ollama-health.test.js +74 -0
- package/server/lib/orchestration/agent-dispatcher.js +114 -0
- package/server/lib/orchestration/agent-dispatcher.test.js +110 -0
- package/server/lib/orchestration/orchestrator.js +130 -0
- package/server/lib/orchestration/orchestrator.test.js +192 -0
- package/server/lib/orchestration/tmux-manager.js +101 -0
- package/server/lib/orchestration/tmux-manager.test.js +109 -0
- package/server/lib/orchestration/worktree-manager.js +132 -0
- package/server/lib/orchestration/worktree-manager.test.js +129 -0
- package/server/lib/port-guard.js +44 -0
- package/server/lib/port-guard.test.js +65 -0
- package/server/lib/project-scanner.js +37 -2
- package/server/lib/project-scanner.test.js +152 -0
- package/server/lib/remember-command.js +2 -0
- package/server/lib/remember-command.test.js +23 -0
- package/server/lib/review/plan-reviewer.js +260 -0
- package/server/lib/review/plan-reviewer.test.js +269 -0
- package/server/lib/review/review-schemas.js +173 -0
- package/server/lib/review/review-schemas.test.js +152 -0
- package/server/lib/security/crypto-utils.test.js +2 -2
- package/server/lib/semantic-recall.js +1 -1
- package/server/lib/semantic-recall.test.js +17 -0
- package/server/lib/ssh-client.js +184 -0
- package/server/lib/ssh-client.test.js +127 -0
- package/server/lib/vps-api.js +184 -0
- package/server/lib/vps-api.test.js +208 -0
- package/server/lib/vps-bootstrap.js +124 -0
- package/server/lib/vps-bootstrap.test.js +79 -0
- package/server/lib/vps-monitor.js +126 -0
- package/server/lib/vps-monitor.test.js +98 -0
- package/server/lib/workspace-api.js +182 -1
- package/server/lib/workspace-api.test.js +474 -0
- package/server/package-lock.json +737 -0
- package/server/package.json +3 -0
- package/server/setup.sh +271 -271
- package/dashboard-web/dist/assets/index-Uhc49PE-.css +0 -1
- package/dashboard-web/dist/assets/index-W36XHPC5.js +0 -431
- package/dashboard-web/dist/assets/index-W36XHPC5.js.map +0 -1
|
@@ -740,4 +740,478 @@ describe('Workspace API', () => {
|
|
|
740
740
|
expect(repo.phaseName).toBe('Auth');
|
|
741
741
|
});
|
|
742
742
|
});
|
|
743
|
+
|
|
744
|
+
// =========================================================================
|
|
745
|
+
// Memory API routes (Task 1 - Phase 77)
|
|
746
|
+
// =========================================================================
|
|
747
|
+
|
|
748
|
+
describe('Memory API routes', () => {
|
|
749
|
+
function createRouterWithMemory(projectPath, memoryApi) {
|
|
750
|
+
const projectId = Buffer.from(projectPath).toString('base64url');
|
|
751
|
+
const mockConfig = createMockGlobalConfig([path.dirname(projectPath)]);
|
|
752
|
+
const mockScanner = createMockProjectScanner([
|
|
753
|
+
{ name: 'mem-project', path: projectPath, hasTlc: true, hasPlanning: true },
|
|
754
|
+
]);
|
|
755
|
+
const router = createWorkspaceRouter({
|
|
756
|
+
globalConfig: mockConfig,
|
|
757
|
+
projectScanner: mockScanner,
|
|
758
|
+
memoryApi,
|
|
759
|
+
});
|
|
760
|
+
return { router, projectId };
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
it('GET /projects/:id/memory/decisions returns decisions from file adapter', async () => {
|
|
764
|
+
const projectPath = path.join(tempDir, 'mem-proj');
|
|
765
|
+
fs.mkdirSync(projectPath, { recursive: true });
|
|
766
|
+
const mockMemoryApi = {};
|
|
767
|
+
const { router, projectId } = createRouterWithMemory(projectPath, mockMemoryApi);
|
|
768
|
+
|
|
769
|
+
const handler = getHandler(router, 'GET', '/projects/:projectId/memory/decisions');
|
|
770
|
+
expect(handler).not.toBeNull();
|
|
771
|
+
|
|
772
|
+
const { req, res } = createMockReqRes('GET', `/projects/${projectId}/memory/decisions`, {}, { projectId });
|
|
773
|
+
await handler(req, res);
|
|
774
|
+
|
|
775
|
+
expect(res.statusCode).toBe(200);
|
|
776
|
+
expect(res._json.decisions).toBeDefined();
|
|
777
|
+
expect(Array.isArray(res._json.decisions)).toBe(true);
|
|
778
|
+
});
|
|
779
|
+
|
|
780
|
+
it('GET /projects/:id/memory/gotchas returns gotchas from file adapter', async () => {
|
|
781
|
+
const projectPath = path.join(tempDir, 'mem-proj2');
|
|
782
|
+
fs.mkdirSync(projectPath, { recursive: true });
|
|
783
|
+
const mockMemoryApi = {};
|
|
784
|
+
const { router, projectId } = createRouterWithMemory(projectPath, mockMemoryApi);
|
|
785
|
+
|
|
786
|
+
const handler = getHandler(router, 'GET', '/projects/:projectId/memory/gotchas');
|
|
787
|
+
expect(handler).not.toBeNull();
|
|
788
|
+
|
|
789
|
+
const { req, res } = createMockReqRes('GET', `/projects/${projectId}/memory/gotchas`, {}, { projectId });
|
|
790
|
+
await handler(req, res);
|
|
791
|
+
|
|
792
|
+
expect(res.statusCode).toBe(200);
|
|
793
|
+
expect(res._json.gotchas).toBeDefined();
|
|
794
|
+
expect(Array.isArray(res._json.gotchas)).toBe(true);
|
|
795
|
+
});
|
|
796
|
+
|
|
797
|
+
it('GET /projects/:id/memory/stats returns per-project stats', async () => {
|
|
798
|
+
const projectPath = path.join(tempDir, 'mem-proj3');
|
|
799
|
+
fs.mkdirSync(projectPath, { recursive: true });
|
|
800
|
+
const mockMemoryApi = {};
|
|
801
|
+
const { router, projectId } = createRouterWithMemory(projectPath, mockMemoryApi);
|
|
802
|
+
|
|
803
|
+
const handler = getHandler(router, 'GET', '/projects/:projectId/memory/stats');
|
|
804
|
+
expect(handler).not.toBeNull();
|
|
805
|
+
|
|
806
|
+
const { req, res } = createMockReqRes('GET', `/projects/${projectId}/memory/stats`, {}, { projectId });
|
|
807
|
+
await handler(req, res);
|
|
808
|
+
|
|
809
|
+
expect(res.statusCode).toBe(200);
|
|
810
|
+
expect(res._json).toHaveProperty('decisions');
|
|
811
|
+
expect(res._json).toHaveProperty('gotchas');
|
|
812
|
+
expect(res._json).toHaveProperty('total');
|
|
813
|
+
});
|
|
814
|
+
|
|
815
|
+
it('memory routes return 404 for unknown project', async () => {
|
|
816
|
+
const mockMemoryApi = {};
|
|
817
|
+
const unknownId = Buffer.from('/tmp/nonexistent').toString('base64url');
|
|
818
|
+
const mockConfig = createMockGlobalConfig([tempDir]);
|
|
819
|
+
const mockScanner = createMockProjectScanner([]);
|
|
820
|
+
const router = createWorkspaceRouter({
|
|
821
|
+
globalConfig: mockConfig,
|
|
822
|
+
projectScanner: mockScanner,
|
|
823
|
+
memoryApi: mockMemoryApi,
|
|
824
|
+
});
|
|
825
|
+
|
|
826
|
+
const handler = getHandler(router, 'GET', '/projects/:projectId/memory/decisions');
|
|
827
|
+
const { req, res } = createMockReqRes('GET', `/projects/${unknownId}/memory/decisions`, {}, { projectId: unknownId });
|
|
828
|
+
await handler(req, res);
|
|
829
|
+
|
|
830
|
+
expect(res.statusCode).toBe(404);
|
|
831
|
+
});
|
|
832
|
+
});
|
|
833
|
+
|
|
834
|
+
// =========================================================================
|
|
835
|
+
// Project file endpoint (Task 1 - Phase 77)
|
|
836
|
+
// =========================================================================
|
|
837
|
+
|
|
838
|
+
describe('Project file endpoint', () => {
|
|
839
|
+
it('GET /projects/:id/files/:filename returns .planning file content', async () => {
|
|
840
|
+
const projectPath = path.join(tempDir, 'file-proj');
|
|
841
|
+
fs.mkdirSync(path.join(projectPath, '.planning'), { recursive: true });
|
|
842
|
+
fs.writeFileSync(path.join(projectPath, '.planning', 'ROADMAP.md'), '# Roadmap\n\nPhase 1');
|
|
843
|
+
|
|
844
|
+
const projectId = Buffer.from(projectPath).toString('base64url');
|
|
845
|
+
const mockConfig = createMockGlobalConfig([tempDir]);
|
|
846
|
+
const mockScanner = createMockProjectScanner([
|
|
847
|
+
{ name: 'file-proj', path: projectPath, hasTlc: true, hasPlanning: true },
|
|
848
|
+
]);
|
|
849
|
+
const router = createWorkspaceRouter({ globalConfig: mockConfig, projectScanner: mockScanner });
|
|
850
|
+
|
|
851
|
+
const handler = getHandler(router, 'GET', '/projects/:projectId/files/:filename');
|
|
852
|
+
expect(handler).not.toBeNull();
|
|
853
|
+
|
|
854
|
+
const { req, res } = createMockReqRes('GET', `/projects/${projectId}/files/ROADMAP.md`, {}, { projectId, filename: 'ROADMAP.md' });
|
|
855
|
+
await handler(req, res);
|
|
856
|
+
|
|
857
|
+
expect(res.statusCode).toBe(200);
|
|
858
|
+
expect(res._json.content).toBe('# Roadmap\n\nPhase 1');
|
|
859
|
+
expect(res._json.filename).toBe('ROADMAP.md');
|
|
860
|
+
});
|
|
861
|
+
|
|
862
|
+
it('file endpoint returns 404 for missing file', async () => {
|
|
863
|
+
const projectPath = path.join(tempDir, 'file-proj2');
|
|
864
|
+
fs.mkdirSync(path.join(projectPath, '.planning'), { recursive: true });
|
|
865
|
+
|
|
866
|
+
const projectId = Buffer.from(projectPath).toString('base64url');
|
|
867
|
+
const mockConfig = createMockGlobalConfig([tempDir]);
|
|
868
|
+
const mockScanner = createMockProjectScanner([
|
|
869
|
+
{ name: 'file-proj2', path: projectPath, hasTlc: true, hasPlanning: true },
|
|
870
|
+
]);
|
|
871
|
+
const router = createWorkspaceRouter({ globalConfig: mockConfig, projectScanner: mockScanner });
|
|
872
|
+
|
|
873
|
+
const handler = getHandler(router, 'GET', '/projects/:projectId/files/:filename');
|
|
874
|
+
const { req, res } = createMockReqRes('GET', `/projects/${projectId}/files/NONEXISTENT.md`, {}, { projectId, filename: 'NONEXISTENT.md' });
|
|
875
|
+
await handler(req, res);
|
|
876
|
+
|
|
877
|
+
expect(res.statusCode).toBe(404);
|
|
878
|
+
});
|
|
879
|
+
|
|
880
|
+
it('file endpoint rejects path traversal', async () => {
|
|
881
|
+
const projectPath = path.join(tempDir, 'file-proj3');
|
|
882
|
+
fs.mkdirSync(path.join(projectPath, '.planning'), { recursive: true });
|
|
883
|
+
|
|
884
|
+
const projectId = Buffer.from(projectPath).toString('base64url');
|
|
885
|
+
const mockConfig = createMockGlobalConfig([tempDir]);
|
|
886
|
+
const mockScanner = createMockProjectScanner([
|
|
887
|
+
{ name: 'file-proj3', path: projectPath, hasTlc: true, hasPlanning: true },
|
|
888
|
+
]);
|
|
889
|
+
const router = createWorkspaceRouter({ globalConfig: mockConfig, projectScanner: mockScanner });
|
|
890
|
+
|
|
891
|
+
const handler = getHandler(router, 'GET', '/projects/:projectId/files/:filename');
|
|
892
|
+
const { req, res } = createMockReqRes('GET', `/projects/${projectId}/files/../../../etc/passwd`, {}, { projectId, filename: '../../../etc/passwd' });
|
|
893
|
+
await handler(req, res);
|
|
894
|
+
|
|
895
|
+
expect(res.statusCode).toBe(400);
|
|
896
|
+
});
|
|
897
|
+
});
|
|
898
|
+
|
|
899
|
+
// =========================================================================
|
|
900
|
+
// Coverage in project status (Task 9 - Phase 77)
|
|
901
|
+
// =========================================================================
|
|
902
|
+
|
|
903
|
+
describe('readProjectStatus coverage', () => {
|
|
904
|
+
it('returns coverage from coverage-summary.json when present', async () => {
|
|
905
|
+
const projectPath = path.join(tempDir, 'cov-project');
|
|
906
|
+
fs.mkdirSync(projectPath, { recursive: true });
|
|
907
|
+
fs.mkdirSync(path.join(projectPath, '.planning'), { recursive: true });
|
|
908
|
+
fs.writeFileSync(path.join(projectPath, '.tlc.json'), '{}');
|
|
909
|
+
fs.mkdirSync(path.join(projectPath, 'coverage'), { recursive: true });
|
|
910
|
+
fs.writeFileSync(
|
|
911
|
+
path.join(projectPath, 'coverage', 'coverage-summary.json'),
|
|
912
|
+
JSON.stringify({
|
|
913
|
+
total: {
|
|
914
|
+
lines: { pct: 85.5 },
|
|
915
|
+
statements: { pct: 82.3 },
|
|
916
|
+
functions: { pct: 90.1 },
|
|
917
|
+
branches: { pct: 70.2 },
|
|
918
|
+
},
|
|
919
|
+
})
|
|
920
|
+
);
|
|
921
|
+
|
|
922
|
+
const projectId = Buffer.from(projectPath).toString('base64url');
|
|
923
|
+
const mockConfig = createMockGlobalConfig([tempDir]);
|
|
924
|
+
const mockScanner = createMockProjectScanner([
|
|
925
|
+
{ name: 'cov-project', path: projectPath, hasTlc: true, hasPlanning: true },
|
|
926
|
+
]);
|
|
927
|
+
const router = createWorkspaceRouter({ globalConfig: mockConfig, projectScanner: mockScanner });
|
|
928
|
+
|
|
929
|
+
const handler = getHandler(router, 'GET', '/projects/:projectId/status');
|
|
930
|
+
const { req, res } = createMockReqRes('GET', `/projects/${projectId}/status`, {}, { projectId });
|
|
931
|
+
await handler(req, res);
|
|
932
|
+
|
|
933
|
+
expect(res.statusCode).toBe(200);
|
|
934
|
+
expect(res._json.status.coverage).toBe(85.5);
|
|
935
|
+
});
|
|
936
|
+
|
|
937
|
+
it('returns null coverage when no coverage-summary.json exists', async () => {
|
|
938
|
+
const projectPath = path.join(tempDir, 'no-cov-project');
|
|
939
|
+
fs.mkdirSync(projectPath, { recursive: true });
|
|
940
|
+
fs.mkdirSync(path.join(projectPath, '.planning'), { recursive: true });
|
|
941
|
+
fs.writeFileSync(path.join(projectPath, '.tlc.json'), '{}');
|
|
942
|
+
|
|
943
|
+
const projectId = Buffer.from(projectPath).toString('base64url');
|
|
944
|
+
const mockConfig = createMockGlobalConfig([tempDir]);
|
|
945
|
+
const mockScanner = createMockProjectScanner([
|
|
946
|
+
{ name: 'no-cov-project', path: projectPath, hasTlc: true, hasPlanning: true },
|
|
947
|
+
]);
|
|
948
|
+
const router = createWorkspaceRouter({ globalConfig: mockConfig, projectScanner: mockScanner });
|
|
949
|
+
|
|
950
|
+
const handler = getHandler(router, 'GET', '/projects/:projectId/status');
|
|
951
|
+
const { req, res } = createMockReqRes('GET', `/projects/${projectId}/status`, {}, { projectId });
|
|
952
|
+
await handler(req, res);
|
|
953
|
+
|
|
954
|
+
expect(res.statusCode).toBe(200);
|
|
955
|
+
expect(res._json.status.coverage).toBeNull();
|
|
956
|
+
});
|
|
957
|
+
|
|
958
|
+
it('returns null coverage when coverage-summary.json is malformed', async () => {
|
|
959
|
+
const projectPath = path.join(tempDir, 'bad-cov-project');
|
|
960
|
+
fs.mkdirSync(projectPath, { recursive: true });
|
|
961
|
+
fs.mkdirSync(path.join(projectPath, '.planning'), { recursive: true });
|
|
962
|
+
fs.writeFileSync(path.join(projectPath, '.tlc.json'), '{}');
|
|
963
|
+
fs.mkdirSync(path.join(projectPath, 'coverage'), { recursive: true });
|
|
964
|
+
fs.writeFileSync(
|
|
965
|
+
path.join(projectPath, 'coverage', 'coverage-summary.json'),
|
|
966
|
+
'not json'
|
|
967
|
+
);
|
|
968
|
+
|
|
969
|
+
const projectId = Buffer.from(projectPath).toString('base64url');
|
|
970
|
+
const mockConfig = createMockGlobalConfig([tempDir]);
|
|
971
|
+
const mockScanner = createMockProjectScanner([
|
|
972
|
+
{ name: 'bad-cov-project', path: projectPath, hasTlc: true, hasPlanning: true },
|
|
973
|
+
]);
|
|
974
|
+
const router = createWorkspaceRouter({ globalConfig: mockConfig, projectScanner: mockScanner });
|
|
975
|
+
|
|
976
|
+
const handler = getHandler(router, 'GET', '/projects/:projectId/status');
|
|
977
|
+
const { req, res } = createMockReqRes('GET', `/projects/${projectId}/status`, {}, { projectId });
|
|
978
|
+
await handler(req, res);
|
|
979
|
+
|
|
980
|
+
expect(res.statusCode).toBe(200);
|
|
981
|
+
expect(res._json.status.coverage).toBeNull();
|
|
982
|
+
});
|
|
983
|
+
});
|
|
984
|
+
|
|
985
|
+
// =========================================================================
|
|
986
|
+
// Phase 79 — Task 5: Memory capture endpoint
|
|
987
|
+
// =========================================================================
|
|
988
|
+
describe('POST /projects/:projectId/memory/capture', () => {
|
|
989
|
+
it('returns 404 for unknown project', async () => {
|
|
990
|
+
const mockConfig = createMockGlobalConfig([tempDir]);
|
|
991
|
+
const mockScanner = createMockProjectScanner([]);
|
|
992
|
+
const mockMemoryApi = {};
|
|
993
|
+
const router = createWorkspaceRouter({
|
|
994
|
+
globalConfig: mockConfig,
|
|
995
|
+
projectScanner: mockScanner,
|
|
996
|
+
memoryApi: mockMemoryApi,
|
|
997
|
+
memoryDeps: {},
|
|
998
|
+
});
|
|
999
|
+
|
|
1000
|
+
const handler = getHandler(router, 'POST', '/projects/:projectId/memory/capture');
|
|
1001
|
+
expect(handler).not.toBeNull();
|
|
1002
|
+
|
|
1003
|
+
const fakeId = Buffer.from('/nonexistent').toString('base64url');
|
|
1004
|
+
const { req, res } = createMockReqRes('POST', `/projects/${fakeId}/memory/capture`, { exchanges: [{ user: 'hello', assistant: 'hi there' }] }, { projectId: fakeId });
|
|
1005
|
+
await handler(req, res);
|
|
1006
|
+
|
|
1007
|
+
expect(res.statusCode).toBe(404);
|
|
1008
|
+
});
|
|
1009
|
+
|
|
1010
|
+
it('returns 400 for missing exchanges body', async () => {
|
|
1011
|
+
const projectPath = path.join(tempDir, 'capture-project');
|
|
1012
|
+
fs.mkdirSync(projectPath, { recursive: true });
|
|
1013
|
+
fs.writeFileSync(path.join(projectPath, '.tlc.json'), '{}');
|
|
1014
|
+
fs.mkdirSync(path.join(projectPath, '.planning'), { recursive: true });
|
|
1015
|
+
|
|
1016
|
+
const projectId = Buffer.from(projectPath).toString('base64url');
|
|
1017
|
+
const mockConfig = createMockGlobalConfig([tempDir]);
|
|
1018
|
+
const mockScanner = createMockProjectScanner([
|
|
1019
|
+
{ name: 'capture-project', path: projectPath, hasTlc: true, hasPlanning: true },
|
|
1020
|
+
]);
|
|
1021
|
+
const router = createWorkspaceRouter({
|
|
1022
|
+
globalConfig: mockConfig,
|
|
1023
|
+
projectScanner: mockScanner,
|
|
1024
|
+
memoryApi: {},
|
|
1025
|
+
memoryDeps: {},
|
|
1026
|
+
});
|
|
1027
|
+
|
|
1028
|
+
const handler = getHandler(router, 'POST', '/projects/:projectId/memory/capture');
|
|
1029
|
+
const { req, res } = createMockReqRes('POST', `/projects/${projectId}/memory/capture`, {}, { projectId });
|
|
1030
|
+
await handler(req, res);
|
|
1031
|
+
|
|
1032
|
+
expect(res.statusCode).toBe(400);
|
|
1033
|
+
});
|
|
1034
|
+
|
|
1035
|
+
it('accepts exchanges and returns captured count', async () => {
|
|
1036
|
+
const projectPath = path.join(tempDir, 'capture-ok');
|
|
1037
|
+
fs.mkdirSync(projectPath, { recursive: true });
|
|
1038
|
+
fs.writeFileSync(path.join(projectPath, '.tlc.json'), '{}');
|
|
1039
|
+
fs.mkdirSync(path.join(projectPath, '.planning'), { recursive: true });
|
|
1040
|
+
|
|
1041
|
+
const projectId = Buffer.from(projectPath).toString('base64url');
|
|
1042
|
+
const mockConfig = createMockGlobalConfig([tempDir]);
|
|
1043
|
+
const mockScanner = createMockProjectScanner([
|
|
1044
|
+
{ name: 'capture-ok', path: projectPath, hasTlc: true, hasPlanning: true },
|
|
1045
|
+
]);
|
|
1046
|
+
|
|
1047
|
+
const observerCalls = [];
|
|
1048
|
+
const mockMemoryDeps = {
|
|
1049
|
+
observeAndRemember: async (root, exchange) => { observerCalls.push({ root, exchange }); },
|
|
1050
|
+
vectorIndexer: null,
|
|
1051
|
+
};
|
|
1052
|
+
const router = createWorkspaceRouter({
|
|
1053
|
+
globalConfig: mockConfig,
|
|
1054
|
+
projectScanner: mockScanner,
|
|
1055
|
+
memoryApi: {},
|
|
1056
|
+
memoryDeps: mockMemoryDeps,
|
|
1057
|
+
});
|
|
1058
|
+
|
|
1059
|
+
const handler = getHandler(router, 'POST', '/projects/:projectId/memory/capture');
|
|
1060
|
+
const exchanges = [
|
|
1061
|
+
{ user: 'what is TLC?', assistant: 'TLC is a test-led coding tool' },
|
|
1062
|
+
{ user: 'how do I use it?', assistant: 'Run /tlc to start' },
|
|
1063
|
+
];
|
|
1064
|
+
const { req, res } = createMockReqRes('POST', `/projects/${projectId}/memory/capture`, { exchanges }, { projectId });
|
|
1065
|
+
await handler(req, res);
|
|
1066
|
+
|
|
1067
|
+
expect(res.statusCode).toBe(200);
|
|
1068
|
+
expect(res._json.captured).toBe(2);
|
|
1069
|
+
});
|
|
1070
|
+
});
|
|
1071
|
+
|
|
1072
|
+
// =========================================================================
|
|
1073
|
+
// Phase 79 — Task 6: Memory search endpoint
|
|
1074
|
+
// =========================================================================
|
|
1075
|
+
describe('GET /projects/:projectId/memory/search', () => {
|
|
1076
|
+
it('returns 404 for unknown project', async () => {
|
|
1077
|
+
const mockConfig = createMockGlobalConfig([tempDir]);
|
|
1078
|
+
const mockScanner = createMockProjectScanner([]);
|
|
1079
|
+
const router = createWorkspaceRouter({
|
|
1080
|
+
globalConfig: mockConfig,
|
|
1081
|
+
projectScanner: mockScanner,
|
|
1082
|
+
memoryApi: {},
|
|
1083
|
+
memoryDeps: {},
|
|
1084
|
+
});
|
|
1085
|
+
|
|
1086
|
+
const handler = getHandler(router, 'GET', '/projects/:projectId/memory/search');
|
|
1087
|
+
expect(handler).not.toBeNull();
|
|
1088
|
+
|
|
1089
|
+
const fakeId = Buffer.from('/nonexistent').toString('base64url');
|
|
1090
|
+
const { req, res } = createMockReqRes('GET', `/projects/${fakeId}/memory/search`, {}, { projectId: fakeId }, { q: 'test' });
|
|
1091
|
+
await handler(req, res);
|
|
1092
|
+
|
|
1093
|
+
expect(res.statusCode).toBe(404);
|
|
1094
|
+
});
|
|
1095
|
+
|
|
1096
|
+
it('returns 400 for missing query parameter', async () => {
|
|
1097
|
+
const projectPath = path.join(tempDir, 'search-project');
|
|
1098
|
+
fs.mkdirSync(projectPath, { recursive: true });
|
|
1099
|
+
fs.writeFileSync(path.join(projectPath, '.tlc.json'), '{}');
|
|
1100
|
+
fs.mkdirSync(path.join(projectPath, '.planning'), { recursive: true });
|
|
1101
|
+
|
|
1102
|
+
const projectId = Buffer.from(projectPath).toString('base64url');
|
|
1103
|
+
const mockConfig = createMockGlobalConfig([tempDir]);
|
|
1104
|
+
const mockScanner = createMockProjectScanner([
|
|
1105
|
+
{ name: 'search-project', path: projectPath, hasTlc: true, hasPlanning: true },
|
|
1106
|
+
]);
|
|
1107
|
+
const router = createWorkspaceRouter({
|
|
1108
|
+
globalConfig: mockConfig,
|
|
1109
|
+
projectScanner: mockScanner,
|
|
1110
|
+
memoryApi: {},
|
|
1111
|
+
memoryDeps: {},
|
|
1112
|
+
});
|
|
1113
|
+
|
|
1114
|
+
const handler = getHandler(router, 'GET', '/projects/:projectId/memory/search');
|
|
1115
|
+
const { req, res } = createMockReqRes('GET', `/projects/${projectId}/memory/search`, {}, { projectId }, {});
|
|
1116
|
+
await handler(req, res);
|
|
1117
|
+
|
|
1118
|
+
expect(res.statusCode).toBe(400);
|
|
1119
|
+
});
|
|
1120
|
+
|
|
1121
|
+
it('returns search results with source indicator using semantic recall', async () => {
|
|
1122
|
+
const projectPath = path.join(tempDir, 'search-ok');
|
|
1123
|
+
fs.mkdirSync(projectPath, { recursive: true });
|
|
1124
|
+
fs.writeFileSync(path.join(projectPath, '.tlc.json'), '{}');
|
|
1125
|
+
fs.mkdirSync(path.join(projectPath, '.planning'), { recursive: true });
|
|
1126
|
+
|
|
1127
|
+
const projectId = Buffer.from(projectPath).toString('base64url');
|
|
1128
|
+
const mockConfig = createMockGlobalConfig([tempDir]);
|
|
1129
|
+
const mockScanner = createMockProjectScanner([
|
|
1130
|
+
{ name: 'search-ok', path: projectPath, hasTlc: true, hasPlanning: true },
|
|
1131
|
+
]);
|
|
1132
|
+
|
|
1133
|
+
const mockMemoryDeps = {
|
|
1134
|
+
semanticRecall: {
|
|
1135
|
+
recall: async (query) => [{ text: 'remembered item', score: 0.92, type: 'decision' }],
|
|
1136
|
+
},
|
|
1137
|
+
};
|
|
1138
|
+
const router = createWorkspaceRouter({
|
|
1139
|
+
globalConfig: mockConfig,
|
|
1140
|
+
projectScanner: mockScanner,
|
|
1141
|
+
memoryApi: {},
|
|
1142
|
+
memoryDeps: mockMemoryDeps,
|
|
1143
|
+
});
|
|
1144
|
+
|
|
1145
|
+
const handler = getHandler(router, 'GET', '/projects/:projectId/memory/search');
|
|
1146
|
+
const { req, res } = createMockReqRes('GET', `/projects/${projectId}/memory/search`, {}, { projectId }, { q: 'remembered' });
|
|
1147
|
+
await handler(req, res);
|
|
1148
|
+
|
|
1149
|
+
expect(res.statusCode).toBe(200);
|
|
1150
|
+
expect(res._json.results).toBeInstanceOf(Array);
|
|
1151
|
+
expect(res._json.source).toBe('vector');
|
|
1152
|
+
});
|
|
1153
|
+
|
|
1154
|
+
it('falls back to file-based search when semantic recall unavailable', async () => {
|
|
1155
|
+
const projectPath = path.join(tempDir, 'search-fallback');
|
|
1156
|
+
fs.mkdirSync(projectPath, { recursive: true });
|
|
1157
|
+
fs.writeFileSync(path.join(projectPath, '.tlc.json'), '{}');
|
|
1158
|
+
fs.mkdirSync(path.join(projectPath, '.planning'), { recursive: true });
|
|
1159
|
+
// Create a memory file for file-based search to find
|
|
1160
|
+
fs.mkdirSync(path.join(projectPath, '.planning', 'memory'), { recursive: true });
|
|
1161
|
+
|
|
1162
|
+
const projectId = Buffer.from(projectPath).toString('base64url');
|
|
1163
|
+
const mockConfig = createMockGlobalConfig([tempDir]);
|
|
1164
|
+
const mockScanner = createMockProjectScanner([
|
|
1165
|
+
{ name: 'search-fallback', path: projectPath, hasTlc: true, hasPlanning: true },
|
|
1166
|
+
]);
|
|
1167
|
+
|
|
1168
|
+
// No semanticRecall provided — should fall back to file-based
|
|
1169
|
+
const router = createWorkspaceRouter({
|
|
1170
|
+
globalConfig: mockConfig,
|
|
1171
|
+
projectScanner: mockScanner,
|
|
1172
|
+
memoryApi: {},
|
|
1173
|
+
memoryDeps: {},
|
|
1174
|
+
});
|
|
1175
|
+
|
|
1176
|
+
const handler = getHandler(router, 'GET', '/projects/:projectId/memory/search');
|
|
1177
|
+
const { req, res } = createMockReqRes('GET', `/projects/${projectId}/memory/search`, {}, { projectId }, { q: 'something' });
|
|
1178
|
+
await handler(req, res);
|
|
1179
|
+
|
|
1180
|
+
expect(res.statusCode).toBe(200);
|
|
1181
|
+
expect(res._json.results).toBeInstanceOf(Array);
|
|
1182
|
+
expect(res._json.source).toBe('file');
|
|
1183
|
+
});
|
|
1184
|
+
|
|
1185
|
+
it('returns empty results when no matches found', async () => {
|
|
1186
|
+
const projectPath = path.join(tempDir, 'search-empty');
|
|
1187
|
+
fs.mkdirSync(projectPath, { recursive: true });
|
|
1188
|
+
fs.writeFileSync(path.join(projectPath, '.tlc.json'), '{}');
|
|
1189
|
+
fs.mkdirSync(path.join(projectPath, '.planning'), { recursive: true });
|
|
1190
|
+
|
|
1191
|
+
const projectId = Buffer.from(projectPath).toString('base64url');
|
|
1192
|
+
const mockConfig = createMockGlobalConfig([tempDir]);
|
|
1193
|
+
const mockScanner = createMockProjectScanner([
|
|
1194
|
+
{ name: 'search-empty', path: projectPath, hasTlc: true, hasPlanning: true },
|
|
1195
|
+
]);
|
|
1196
|
+
|
|
1197
|
+
const mockMemoryDeps = {
|
|
1198
|
+
semanticRecall: {
|
|
1199
|
+
recall: async () => [],
|
|
1200
|
+
},
|
|
1201
|
+
};
|
|
1202
|
+
const router = createWorkspaceRouter({
|
|
1203
|
+
globalConfig: mockConfig,
|
|
1204
|
+
projectScanner: mockScanner,
|
|
1205
|
+
memoryApi: {},
|
|
1206
|
+
memoryDeps: mockMemoryDeps,
|
|
1207
|
+
});
|
|
1208
|
+
|
|
1209
|
+
const handler = getHandler(router, 'GET', '/projects/:projectId/memory/search');
|
|
1210
|
+
const { req, res } = createMockReqRes('GET', `/projects/${projectId}/memory/search`, {}, { projectId }, { q: 'nonexistent' });
|
|
1211
|
+
await handler(req, res);
|
|
1212
|
+
|
|
1213
|
+
expect(res.statusCode).toBe(200);
|
|
1214
|
+
expect(res._json.results).toEqual([]);
|
|
1215
|
+
});
|
|
1216
|
+
});
|
|
743
1217
|
});
|