2026-02-24 14:53:00 +00:00
|
|
|
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
|
|
|
import { PromptService } from '../../src/services/prompt.service.js';
|
|
|
|
|
import type { IPromptRepository } from '../../src/repositories/prompt.repository.js';
|
|
|
|
|
import type { IPromptRequestRepository } from '../../src/repositories/prompt-request.repository.js';
|
|
|
|
|
import type { IProjectRepository } from '../../src/repositories/project.repository.js';
|
|
|
|
|
import type { Prompt, PromptRequest, Project } from '@prisma/client';
|
|
|
|
|
|
|
|
|
|
function makePrompt(overrides: Partial<Prompt> = {}): Prompt {
|
|
|
|
|
return {
|
|
|
|
|
id: 'prompt-1',
|
|
|
|
|
name: 'test-prompt',
|
|
|
|
|
content: 'Hello world',
|
|
|
|
|
projectId: null,
|
feat(mcpd): ResourceRevision + ResourceProposal services + Prompt revision integration
Phase 2 of the Skills + Revisions + Proposals work. Stands up the generic
revision/proposal layer and wires Prompt into it. Skills will plug into the
same infrastructure in PR-3 with no further service changes required.
This PR is intentionally additive: PromptRequest table and routes are
unchanged. The /api/v1/proposals API runs side-by-side with the legacy
/api/v1/promptrequests API. The PromptRequest cutover (rename + backfill +
mcplocal rewire) is deferred to a later PR so this one stays reviewable.
## What's added
### Repositories (src/mcpd/src/repositories/)
- resource-revision.repository.ts — append-only revision log keyed by
(resourceType, resourceId). Soft FK; no relations declared. Supports
history listing, semver lookup, and contentHash cross-resource search.
- resource-proposal.repository.ts — generic propose queue. Status lifecycle
pending → approved | rejected. Mirrors Prompt's `?? ''` workaround for
nullable-FK compound lookups.
### Services (src/mcpd/src/services/)
- resource-revision.service.ts — record() inserts a revision with a stable
sha256 contentHash computed from canonicalised JSON (key-sorted at every
level so reordered objects produce the same hash). Caller passes a
pre-computed semver; service does NOT decide bump policy.
- resource-proposal.service.ts — propose / approve / reject / list, with a
per-resourceType handler registry. PromptService registers the 'prompt'
handler at construction; the SkillService will register 'skill' in PR-3.
approve() runs in a Prisma $transaction so the resource update + revision
insert + proposal status flip are atomic.
### Pure utility (src/mcpd/src/utils/semver.ts)
- bumpSemver(current, kind) for major / minor / patch
- compareSemver(a, b) — numeric, not lex (10 > 9)
- isValidSemver(s)
- Invalid input falls back to '0.1.0' rather than throwing — keeps the
audit-write path from blowing up the prompt update if a row's semver
ever drifts out of MAJOR.MINOR.PATCH shape.
### Routes (src/mcpd/src/routes/)
- revisions.ts — GET /api/v1/revisions?resourceType=&resourceId=,
GET /api/v1/revisions/:id, GET /api/v1/revisions/:id/diff?against=<id|live>
(unified-format diff via the `diff` package), and POST
/api/v1/prompts/:id/restore-revision { revisionId, note? }.
- proposals.ts — GET / POST /api/v1/proposals,
GET /api/v1/proposals/:id, PUT for body updates, POST .../approve and
POST .../reject, plus DELETE.
## What's changed
- PromptService.create / update now record a ResourceRevision when the
revision service is wired. Update auto-bumps patch on content change;
authors can override via `--bump major|minor|patch` or `--semver X.Y.Z`
on the CLI (forwarded into the PUT body). Best-effort: revision write
failures are swallowed so the prompt save still succeeds (revision is
audit, not source of truth).
- PromptService.setProposalService registers a 'prompt' approval handler
with the proposal service. Approval runs in a Prisma transaction:
upsert prompt → record revision → update currentRevisionId → flip
proposal status. semver bumps to 0.1.0 on first approval, patch
thereafter.
- New CLI flags on `mcpctl edit prompt`: --bump, --semver, --note. They're
prompt-only (validated client-side); other resources reject them.
- Aliases in shared.ts: `proposal`/`prop` → proposals,
`revision`/`rev` → revisions.
- diff dependency added to mcpd.
## Tests
- src/mcpd/tests/utils/semver.test.ts — covers bump/compare/validate
including numeric (not lex) semver compare and invalid-input fallback.
- prompt-service.test.ts updated: makePrompt fixture now sets semver +
agentId + currentRevisionId; updatePrompt assertion expects the
auto-bumped patch in the same update call.
- prompt-routes.test.ts updated symmetrically.
## RBAC
`proposals` and `revisions` URL segments map to the existing `prompts`
permission for now. PR-7 may split if a "reviewer" role becomes useful.
## Verification
Full suite: 158 test files / 2127 tests green.
`pnpm build` clean across all 6 workspace packages.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 00:38:35 +01:00
|
|
|
agentId: null,
|
feat: gated project experience & prompt intelligence
Implements the full gated session flow and prompt intelligence system:
- Prisma schema: add gated, priority, summary, chapters, linkTarget fields
- Session gate: state machine (gated → begin_session → ungated) with LLM-powered
tool selection based on prompt index
- Tag matcher: intelligent prompt-to-tool matching with project/server/action tags
- LLM selector: tiered provider selection (fast for gating, heavy for complex tasks)
- Link resolver: cross-project MCP resource references (project/server:uri format)
- Prompt summary service: LLM-generated summaries and chapter extraction
- System project bootstrap: ensures default project exists on startup
- Structural link health checks: enrichWithLinkStatus on prompt GET endpoints
- CLI: create prompt --priority/--link, create project --gated/--no-gated,
describe project shows prompts section, get prompts shows PRI/LINK/STATUS
- Apply/edit: priority, linkTarget, gated fields supported
- Shell completions: fish updated with new flags
- 1,253 tests passing across all packages
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-25 23:22:42 +00:00
|
|
|
priority: 5,
|
|
|
|
|
summary: null,
|
|
|
|
|
chapters: null,
|
|
|
|
|
linkTarget: null,
|
feat(mcpd): ResourceRevision + ResourceProposal services + Prompt revision integration
Phase 2 of the Skills + Revisions + Proposals work. Stands up the generic
revision/proposal layer and wires Prompt into it. Skills will plug into the
same infrastructure in PR-3 with no further service changes required.
This PR is intentionally additive: PromptRequest table and routes are
unchanged. The /api/v1/proposals API runs side-by-side with the legacy
/api/v1/promptrequests API. The PromptRequest cutover (rename + backfill +
mcplocal rewire) is deferred to a later PR so this one stays reviewable.
## What's added
### Repositories (src/mcpd/src/repositories/)
- resource-revision.repository.ts — append-only revision log keyed by
(resourceType, resourceId). Soft FK; no relations declared. Supports
history listing, semver lookup, and contentHash cross-resource search.
- resource-proposal.repository.ts — generic propose queue. Status lifecycle
pending → approved | rejected. Mirrors Prompt's `?? ''` workaround for
nullable-FK compound lookups.
### Services (src/mcpd/src/services/)
- resource-revision.service.ts — record() inserts a revision with a stable
sha256 contentHash computed from canonicalised JSON (key-sorted at every
level so reordered objects produce the same hash). Caller passes a
pre-computed semver; service does NOT decide bump policy.
- resource-proposal.service.ts — propose / approve / reject / list, with a
per-resourceType handler registry. PromptService registers the 'prompt'
handler at construction; the SkillService will register 'skill' in PR-3.
approve() runs in a Prisma $transaction so the resource update + revision
insert + proposal status flip are atomic.
### Pure utility (src/mcpd/src/utils/semver.ts)
- bumpSemver(current, kind) for major / minor / patch
- compareSemver(a, b) — numeric, not lex (10 > 9)
- isValidSemver(s)
- Invalid input falls back to '0.1.0' rather than throwing — keeps the
audit-write path from blowing up the prompt update if a row's semver
ever drifts out of MAJOR.MINOR.PATCH shape.
### Routes (src/mcpd/src/routes/)
- revisions.ts — GET /api/v1/revisions?resourceType=&resourceId=,
GET /api/v1/revisions/:id, GET /api/v1/revisions/:id/diff?against=<id|live>
(unified-format diff via the `diff` package), and POST
/api/v1/prompts/:id/restore-revision { revisionId, note? }.
- proposals.ts — GET / POST /api/v1/proposals,
GET /api/v1/proposals/:id, PUT for body updates, POST .../approve and
POST .../reject, plus DELETE.
## What's changed
- PromptService.create / update now record a ResourceRevision when the
revision service is wired. Update auto-bumps patch on content change;
authors can override via `--bump major|minor|patch` or `--semver X.Y.Z`
on the CLI (forwarded into the PUT body). Best-effort: revision write
failures are swallowed so the prompt save still succeeds (revision is
audit, not source of truth).
- PromptService.setProposalService registers a 'prompt' approval handler
with the proposal service. Approval runs in a Prisma transaction:
upsert prompt → record revision → update currentRevisionId → flip
proposal status. semver bumps to 0.1.0 on first approval, patch
thereafter.
- New CLI flags on `mcpctl edit prompt`: --bump, --semver, --note. They're
prompt-only (validated client-side); other resources reject them.
- Aliases in shared.ts: `proposal`/`prop` → proposals,
`revision`/`rev` → revisions.
- diff dependency added to mcpd.
## Tests
- src/mcpd/tests/utils/semver.test.ts — covers bump/compare/validate
including numeric (not lex) semver compare and invalid-input fallback.
- prompt-service.test.ts updated: makePrompt fixture now sets semver +
agentId + currentRevisionId; updatePrompt assertion expects the
auto-bumped patch in the same update call.
- prompt-routes.test.ts updated symmetrically.
## RBAC
`proposals` and `revisions` URL segments map to the existing `prompts`
permission for now. PR-7 may split if a "reviewer" role becomes useful.
## Verification
Full suite: 158 test files / 2127 tests green.
`pnpm build` clean across all 6 workspace packages.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 00:38:35 +01:00
|
|
|
semver: '0.1.0',
|
|
|
|
|
currentRevisionId: null,
|
2026-02-24 14:53:00 +00:00
|
|
|
version: 1,
|
|
|
|
|
createdAt: new Date(),
|
|
|
|
|
updatedAt: new Date(),
|
|
|
|
|
...overrides,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function makePromptRequest(overrides: Partial<PromptRequest> = {}): PromptRequest {
|
|
|
|
|
return {
|
|
|
|
|
id: 'req-1',
|
|
|
|
|
name: 'test-request',
|
|
|
|
|
content: 'Proposed content',
|
|
|
|
|
projectId: null,
|
feat: gated project experience & prompt intelligence
Implements the full gated session flow and prompt intelligence system:
- Prisma schema: add gated, priority, summary, chapters, linkTarget fields
- Session gate: state machine (gated → begin_session → ungated) with LLM-powered
tool selection based on prompt index
- Tag matcher: intelligent prompt-to-tool matching with project/server/action tags
- LLM selector: tiered provider selection (fast for gating, heavy for complex tasks)
- Link resolver: cross-project MCP resource references (project/server:uri format)
- Prompt summary service: LLM-generated summaries and chapter extraction
- System project bootstrap: ensures default project exists on startup
- Structural link health checks: enrichWithLinkStatus on prompt GET endpoints
- CLI: create prompt --priority/--link, create project --gated/--no-gated,
describe project shows prompts section, get prompts shows PRI/LINK/STATUS
- Apply/edit: priority, linkTarget, gated fields supported
- Shell completions: fish updated with new flags
- 1,253 tests passing across all packages
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-25 23:22:42 +00:00
|
|
|
priority: 5,
|
2026-02-24 14:53:00 +00:00
|
|
|
createdBySession: 'session-abc',
|
|
|
|
|
createdByUserId: null,
|
|
|
|
|
createdAt: new Date(),
|
|
|
|
|
...overrides,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function makeProject(overrides: Partial<Project> = {}): Project {
|
|
|
|
|
return {
|
|
|
|
|
id: 'proj-1',
|
|
|
|
|
name: 'test-project',
|
|
|
|
|
description: '',
|
|
|
|
|
prompt: '',
|
2026-03-03 19:07:39 +00:00
|
|
|
proxyModel: '',
|
feat: gated project experience & prompt intelligence
Implements the full gated session flow and prompt intelligence system:
- Prisma schema: add gated, priority, summary, chapters, linkTarget fields
- Session gate: state machine (gated → begin_session → ungated) with LLM-powered
tool selection based on prompt index
- Tag matcher: intelligent prompt-to-tool matching with project/server/action tags
- LLM selector: tiered provider selection (fast for gating, heavy for complex tasks)
- Link resolver: cross-project MCP resource references (project/server:uri format)
- Prompt summary service: LLM-generated summaries and chapter extraction
- System project bootstrap: ensures default project exists on startup
- Structural link health checks: enrichWithLinkStatus on prompt GET endpoints
- CLI: create prompt --priority/--link, create project --gated/--no-gated,
describe project shows prompts section, get prompts shows PRI/LINK/STATUS
- Apply/edit: priority, linkTarget, gated fields supported
- Shell completions: fish updated with new flags
- 1,253 tests passing across all packages
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-25 23:22:42 +00:00
|
|
|
gated: true,
|
2026-02-24 14:53:00 +00:00
|
|
|
llmProvider: null,
|
|
|
|
|
llmModel: null,
|
|
|
|
|
ownerId: 'user-1',
|
|
|
|
|
createdAt: new Date(),
|
|
|
|
|
updatedAt: new Date(),
|
|
|
|
|
...overrides,
|
|
|
|
|
} as Project;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function mockPromptRepo(): IPromptRepository {
|
|
|
|
|
return {
|
|
|
|
|
findAll: vi.fn(async () => []),
|
feat: gated project experience & prompt intelligence
Implements the full gated session flow and prompt intelligence system:
- Prisma schema: add gated, priority, summary, chapters, linkTarget fields
- Session gate: state machine (gated → begin_session → ungated) with LLM-powered
tool selection based on prompt index
- Tag matcher: intelligent prompt-to-tool matching with project/server/action tags
- LLM selector: tiered provider selection (fast for gating, heavy for complex tasks)
- Link resolver: cross-project MCP resource references (project/server:uri format)
- Prompt summary service: LLM-generated summaries and chapter extraction
- System project bootstrap: ensures default project exists on startup
- Structural link health checks: enrichWithLinkStatus on prompt GET endpoints
- CLI: create prompt --priority/--link, create project --gated/--no-gated,
describe project shows prompts section, get prompts shows PRI/LINK/STATUS
- Apply/edit: priority, linkTarget, gated fields supported
- Shell completions: fish updated with new flags
- 1,253 tests passing across all packages
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-25 23:22:42 +00:00
|
|
|
findGlobal: vi.fn(async () => []),
|
2026-02-24 14:53:00 +00:00
|
|
|
findById: vi.fn(async () => null),
|
|
|
|
|
findByNameAndProject: vi.fn(async () => null),
|
|
|
|
|
create: vi.fn(async (data) => makePrompt(data)),
|
|
|
|
|
update: vi.fn(async (id, data) => makePrompt({ id, ...data })),
|
|
|
|
|
delete: vi.fn(async () => {}),
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function mockPromptRequestRepo(): IPromptRequestRepository {
|
|
|
|
|
return {
|
|
|
|
|
findAll: vi.fn(async () => []),
|
feat: gated project experience & prompt intelligence
Implements the full gated session flow and prompt intelligence system:
- Prisma schema: add gated, priority, summary, chapters, linkTarget fields
- Session gate: state machine (gated → begin_session → ungated) with LLM-powered
tool selection based on prompt index
- Tag matcher: intelligent prompt-to-tool matching with project/server/action tags
- LLM selector: tiered provider selection (fast for gating, heavy for complex tasks)
- Link resolver: cross-project MCP resource references (project/server:uri format)
- Prompt summary service: LLM-generated summaries and chapter extraction
- System project bootstrap: ensures default project exists on startup
- Structural link health checks: enrichWithLinkStatus on prompt GET endpoints
- CLI: create prompt --priority/--link, create project --gated/--no-gated,
describe project shows prompts section, get prompts shows PRI/LINK/STATUS
- Apply/edit: priority, linkTarget, gated fields supported
- Shell completions: fish updated with new flags
- 1,253 tests passing across all packages
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-25 23:22:42 +00:00
|
|
|
findGlobal: vi.fn(async () => []),
|
2026-02-24 14:53:00 +00:00
|
|
|
findById: vi.fn(async () => null),
|
|
|
|
|
findByNameAndProject: vi.fn(async () => null),
|
|
|
|
|
findBySession: vi.fn(async () => []),
|
|
|
|
|
create: vi.fn(async (data) => makePromptRequest(data)),
|
feat: gated project experience & prompt intelligence
Implements the full gated session flow and prompt intelligence system:
- Prisma schema: add gated, priority, summary, chapters, linkTarget fields
- Session gate: state machine (gated → begin_session → ungated) with LLM-powered
tool selection based on prompt index
- Tag matcher: intelligent prompt-to-tool matching with project/server/action tags
- LLM selector: tiered provider selection (fast for gating, heavy for complex tasks)
- Link resolver: cross-project MCP resource references (project/server:uri format)
- Prompt summary service: LLM-generated summaries and chapter extraction
- System project bootstrap: ensures default project exists on startup
- Structural link health checks: enrichWithLinkStatus on prompt GET endpoints
- CLI: create prompt --priority/--link, create project --gated/--no-gated,
describe project shows prompts section, get prompts shows PRI/LINK/STATUS
- Apply/edit: priority, linkTarget, gated fields supported
- Shell completions: fish updated with new flags
- 1,253 tests passing across all packages
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-25 23:22:42 +00:00
|
|
|
update: vi.fn(async (id, data) => makePromptRequest({ id, ...data })),
|
2026-02-24 14:53:00 +00:00
|
|
|
delete: vi.fn(async () => {}),
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function mockProjectRepo(): IProjectRepository {
|
|
|
|
|
return {
|
|
|
|
|
findAll: vi.fn(async () => []),
|
|
|
|
|
findById: vi.fn(async () => null),
|
|
|
|
|
findByName: vi.fn(async () => null),
|
|
|
|
|
create: vi.fn(async (data) => makeProject(data)),
|
|
|
|
|
update: vi.fn(async (id, data) => makeProject({ id, ...data })),
|
|
|
|
|
delete: vi.fn(async () => {}),
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
describe('PromptService', () => {
|
|
|
|
|
let promptRepo: IPromptRepository;
|
|
|
|
|
let promptRequestRepo: IPromptRequestRepository;
|
|
|
|
|
let projectRepo: IProjectRepository;
|
|
|
|
|
let service: PromptService;
|
|
|
|
|
|
|
|
|
|
beforeEach(() => {
|
|
|
|
|
promptRepo = mockPromptRepo();
|
|
|
|
|
promptRequestRepo = mockPromptRequestRepo();
|
|
|
|
|
projectRepo = mockProjectRepo();
|
|
|
|
|
service = new PromptService(promptRepo, promptRequestRepo, projectRepo);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// ── Prompt CRUD ──
|
|
|
|
|
|
|
|
|
|
describe('listPrompts', () => {
|
|
|
|
|
it('should return all prompts', async () => {
|
|
|
|
|
const prompts = [makePrompt(), makePrompt({ id: 'prompt-2', name: 'other' })];
|
|
|
|
|
vi.mocked(promptRepo.findAll).mockResolvedValue(prompts);
|
|
|
|
|
|
|
|
|
|
const result = await service.listPrompts();
|
|
|
|
|
expect(result).toEqual(prompts);
|
|
|
|
|
expect(promptRepo.findAll).toHaveBeenCalledWith(undefined);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should filter by projectId', async () => {
|
|
|
|
|
await service.listPrompts('proj-1');
|
|
|
|
|
expect(promptRepo.findAll).toHaveBeenCalledWith('proj-1');
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
feat: gated project experience & prompt intelligence
Implements the full gated session flow and prompt intelligence system:
- Prisma schema: add gated, priority, summary, chapters, linkTarget fields
- Session gate: state machine (gated → begin_session → ungated) with LLM-powered
tool selection based on prompt index
- Tag matcher: intelligent prompt-to-tool matching with project/server/action tags
- LLM selector: tiered provider selection (fast for gating, heavy for complex tasks)
- Link resolver: cross-project MCP resource references (project/server:uri format)
- Prompt summary service: LLM-generated summaries and chapter extraction
- System project bootstrap: ensures default project exists on startup
- Structural link health checks: enrichWithLinkStatus on prompt GET endpoints
- CLI: create prompt --priority/--link, create project --gated/--no-gated,
describe project shows prompts section, get prompts shows PRI/LINK/STATUS
- Apply/edit: priority, linkTarget, gated fields supported
- Shell completions: fish updated with new flags
- 1,253 tests passing across all packages
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-25 23:22:42 +00:00
|
|
|
describe('listGlobalPrompts', () => {
|
|
|
|
|
it('should return only global prompts', async () => {
|
|
|
|
|
const globalPrompts = [makePrompt({ name: 'global-rule', projectId: null })];
|
|
|
|
|
vi.mocked(promptRepo.findGlobal).mockResolvedValue(globalPrompts);
|
|
|
|
|
|
|
|
|
|
const result = await service.listGlobalPrompts();
|
|
|
|
|
expect(result).toEqual(globalPrompts);
|
|
|
|
|
expect(promptRepo.findGlobal).toHaveBeenCalled();
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
2026-02-24 14:53:00 +00:00
|
|
|
describe('getPrompt', () => {
|
|
|
|
|
it('should return a prompt by id', async () => {
|
|
|
|
|
const prompt = makePrompt();
|
|
|
|
|
vi.mocked(promptRepo.findById).mockResolvedValue(prompt);
|
|
|
|
|
|
|
|
|
|
const result = await service.getPrompt('prompt-1');
|
|
|
|
|
expect(result).toEqual(prompt);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should throw NotFoundError for missing prompt', async () => {
|
|
|
|
|
await expect(service.getPrompt('nope')).rejects.toThrow('Prompt not found: nope');
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
describe('createPrompt', () => {
|
|
|
|
|
it('should create a prompt', async () => {
|
|
|
|
|
const result = await service.createPrompt({ name: 'new-prompt', content: 'stuff' });
|
|
|
|
|
expect(promptRepo.create).toHaveBeenCalledWith({ name: 'new-prompt', content: 'stuff' });
|
|
|
|
|
expect(result.name).toBe('new-prompt');
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should validate project exists when projectId given', async () => {
|
|
|
|
|
vi.mocked(projectRepo.findById).mockResolvedValue(makeProject());
|
|
|
|
|
await service.createPrompt({ name: 'scoped', content: 'x', projectId: 'proj-1' });
|
|
|
|
|
expect(projectRepo.findById).toHaveBeenCalledWith('proj-1');
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should throw when project not found', async () => {
|
|
|
|
|
await expect(
|
|
|
|
|
service.createPrompt({ name: 'bad', content: 'x', projectId: 'nope' }),
|
|
|
|
|
).rejects.toThrow('Project not found: nope');
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should reject invalid name format', async () => {
|
|
|
|
|
await expect(
|
|
|
|
|
service.createPrompt({ name: 'INVALID_NAME', content: 'x' }),
|
|
|
|
|
).rejects.toThrow();
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
describe('updatePrompt', () => {
|
|
|
|
|
it('should update prompt content', async () => {
|
|
|
|
|
vi.mocked(promptRepo.findById).mockResolvedValue(makePrompt());
|
|
|
|
|
await service.updatePrompt('prompt-1', { content: 'updated' });
|
feat(mcpd): ResourceRevision + ResourceProposal services + Prompt revision integration
Phase 2 of the Skills + Revisions + Proposals work. Stands up the generic
revision/proposal layer and wires Prompt into it. Skills will plug into the
same infrastructure in PR-3 with no further service changes required.
This PR is intentionally additive: PromptRequest table and routes are
unchanged. The /api/v1/proposals API runs side-by-side with the legacy
/api/v1/promptrequests API. The PromptRequest cutover (rename + backfill +
mcplocal rewire) is deferred to a later PR so this one stays reviewable.
## What's added
### Repositories (src/mcpd/src/repositories/)
- resource-revision.repository.ts — append-only revision log keyed by
(resourceType, resourceId). Soft FK; no relations declared. Supports
history listing, semver lookup, and contentHash cross-resource search.
- resource-proposal.repository.ts — generic propose queue. Status lifecycle
pending → approved | rejected. Mirrors Prompt's `?? ''` workaround for
nullable-FK compound lookups.
### Services (src/mcpd/src/services/)
- resource-revision.service.ts — record() inserts a revision with a stable
sha256 contentHash computed from canonicalised JSON (key-sorted at every
level so reordered objects produce the same hash). Caller passes a
pre-computed semver; service does NOT decide bump policy.
- resource-proposal.service.ts — propose / approve / reject / list, with a
per-resourceType handler registry. PromptService registers the 'prompt'
handler at construction; the SkillService will register 'skill' in PR-3.
approve() runs in a Prisma $transaction so the resource update + revision
insert + proposal status flip are atomic.
### Pure utility (src/mcpd/src/utils/semver.ts)
- bumpSemver(current, kind) for major / minor / patch
- compareSemver(a, b) — numeric, not lex (10 > 9)
- isValidSemver(s)
- Invalid input falls back to '0.1.0' rather than throwing — keeps the
audit-write path from blowing up the prompt update if a row's semver
ever drifts out of MAJOR.MINOR.PATCH shape.
### Routes (src/mcpd/src/routes/)
- revisions.ts — GET /api/v1/revisions?resourceType=&resourceId=,
GET /api/v1/revisions/:id, GET /api/v1/revisions/:id/diff?against=<id|live>
(unified-format diff via the `diff` package), and POST
/api/v1/prompts/:id/restore-revision { revisionId, note? }.
- proposals.ts — GET / POST /api/v1/proposals,
GET /api/v1/proposals/:id, PUT for body updates, POST .../approve and
POST .../reject, plus DELETE.
## What's changed
- PromptService.create / update now record a ResourceRevision when the
revision service is wired. Update auto-bumps patch on content change;
authors can override via `--bump major|minor|patch` or `--semver X.Y.Z`
on the CLI (forwarded into the PUT body). Best-effort: revision write
failures are swallowed so the prompt save still succeeds (revision is
audit, not source of truth).
- PromptService.setProposalService registers a 'prompt' approval handler
with the proposal service. Approval runs in a Prisma transaction:
upsert prompt → record revision → update currentRevisionId → flip
proposal status. semver bumps to 0.1.0 on first approval, patch
thereafter.
- New CLI flags on `mcpctl edit prompt`: --bump, --semver, --note. They're
prompt-only (validated client-side); other resources reject them.
- Aliases in shared.ts: `proposal`/`prop` → proposals,
`revision`/`rev` → revisions.
- diff dependency added to mcpd.
## Tests
- src/mcpd/tests/utils/semver.test.ts — covers bump/compare/validate
including numeric (not lex) semver compare and invalid-input fallback.
- prompt-service.test.ts updated: makePrompt fixture now sets semver +
agentId + currentRevisionId; updatePrompt assertion expects the
auto-bumped patch in the same update call.
- prompt-routes.test.ts updated symmetrically.
## RBAC
`proposals` and `revisions` URL segments map to the existing `prompts`
permission for now. PR-7 may split if a "reviewer" role becomes useful.
## Verification
Full suite: 158 test files / 2127 tests green.
`pnpm build` clean across all 6 workspace packages.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 00:38:35 +01:00
|
|
|
// Auto-patch bump on content change (PR-2): updatePrompt now also
|
|
|
|
|
// emits the new semver in the same update call.
|
|
|
|
|
expect(promptRepo.update).toHaveBeenCalledWith('prompt-1', { content: 'updated', semver: '0.1.1' });
|
2026-02-24 14:53:00 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should throw for missing prompt', async () => {
|
|
|
|
|
await expect(service.updatePrompt('nope', { content: 'x' })).rejects.toThrow('Prompt not found');
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
describe('deletePrompt', () => {
|
|
|
|
|
it('should delete an existing prompt', async () => {
|
|
|
|
|
vi.mocked(promptRepo.findById).mockResolvedValue(makePrompt());
|
|
|
|
|
await service.deletePrompt('prompt-1');
|
|
|
|
|
expect(promptRepo.delete).toHaveBeenCalledWith('prompt-1');
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should throw for missing prompt', async () => {
|
|
|
|
|
await expect(service.deletePrompt('nope')).rejects.toThrow('Prompt not found');
|
|
|
|
|
});
|
feat: gated project experience & prompt intelligence
Implements the full gated session flow and prompt intelligence system:
- Prisma schema: add gated, priority, summary, chapters, linkTarget fields
- Session gate: state machine (gated → begin_session → ungated) with LLM-powered
tool selection based on prompt index
- Tag matcher: intelligent prompt-to-tool matching with project/server/action tags
- LLM selector: tiered provider selection (fast for gating, heavy for complex tasks)
- Link resolver: cross-project MCP resource references (project/server:uri format)
- Prompt summary service: LLM-generated summaries and chapter extraction
- System project bootstrap: ensures default project exists on startup
- Structural link health checks: enrichWithLinkStatus on prompt GET endpoints
- CLI: create prompt --priority/--link, create project --gated/--no-gated,
describe project shows prompts section, get prompts shows PRI/LINK/STATUS
- Apply/edit: priority, linkTarget, gated fields supported
- Shell completions: fish updated with new flags
- 1,253 tests passing across all packages
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-25 23:22:42 +00:00
|
|
|
|
feat: audit console TUI, system prompt management, and CLI improvements
Audit Console Phase 1: tool_call_trace emission from mcplocal router,
session_bind/rbac_decision event kinds, GET /audit/sessions endpoint,
full Ink TUI with session sidebar, event timeline, and detail view
(mcpctl console --audit).
System prompts: move 6 hardcoded LLM prompts to mcpctl-system project
with extensible ResourceRuleRegistry validation framework, template
variable enforcement ({{maxTokens}}, {{pageCount}}), and delete-resets-
to-default behavior. All consumers fetch via SystemPromptFetcher with
hardcoded fallbacks.
CLI: -p shorthand for --project across get/create/delete/config commands,
console auto-scroll improvements, shell completions regenerated.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 23:50:54 +00:00
|
|
|
it('should reset system prompts to default on delete', async () => {
|
|
|
|
|
vi.mocked(promptRepo.findById).mockResolvedValue(makePrompt({ name: 'gate-instructions', projectId: 'sys-proj' }));
|
feat: gated project experience & prompt intelligence
Implements the full gated session flow and prompt intelligence system:
- Prisma schema: add gated, priority, summary, chapters, linkTarget fields
- Session gate: state machine (gated → begin_session → ungated) with LLM-powered
tool selection based on prompt index
- Tag matcher: intelligent prompt-to-tool matching with project/server/action tags
- LLM selector: tiered provider selection (fast for gating, heavy for complex tasks)
- Link resolver: cross-project MCP resource references (project/server:uri format)
- Prompt summary service: LLM-generated summaries and chapter extraction
- System project bootstrap: ensures default project exists on startup
- Structural link health checks: enrichWithLinkStatus on prompt GET endpoints
- CLI: create prompt --priority/--link, create project --gated/--no-gated,
describe project shows prompts section, get prompts shows PRI/LINK/STATUS
- Apply/edit: priority, linkTarget, gated fields supported
- Shell completions: fish updated with new flags
- 1,253 tests passing across all packages
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-25 23:22:42 +00:00
|
|
|
vi.mocked(projectRepo.findById).mockResolvedValue(makeProject({ id: 'sys-proj', name: 'mcpctl-system' }));
|
|
|
|
|
|
feat: audit console TUI, system prompt management, and CLI improvements
Audit Console Phase 1: tool_call_trace emission from mcplocal router,
session_bind/rbac_decision event kinds, GET /audit/sessions endpoint,
full Ink TUI with session sidebar, event timeline, and detail view
(mcpctl console --audit).
System prompts: move 6 hardcoded LLM prompts to mcpctl-system project
with extensible ResourceRuleRegistry validation framework, template
variable enforcement ({{maxTokens}}, {{pageCount}}), and delete-resets-
to-default behavior. All consumers fetch via SystemPromptFetcher with
hardcoded fallbacks.
CLI: -p shorthand for --project across get/create/delete/config commands,
console auto-scroll improvements, shell completions regenerated.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 23:50:54 +00:00
|
|
|
const result = await service.deletePrompt('prompt-1');
|
|
|
|
|
// Should reset via update, not delete
|
|
|
|
|
expect(promptRepo.update).toHaveBeenCalledWith('prompt-1', expect.objectContaining({ content: expect.any(String) }));
|
|
|
|
|
expect(promptRepo.delete).not.toHaveBeenCalled();
|
|
|
|
|
expect(result).toBeDefined();
|
feat: gated project experience & prompt intelligence
Implements the full gated session flow and prompt intelligence system:
- Prisma schema: add gated, priority, summary, chapters, linkTarget fields
- Session gate: state machine (gated → begin_session → ungated) with LLM-powered
tool selection based on prompt index
- Tag matcher: intelligent prompt-to-tool matching with project/server/action tags
- LLM selector: tiered provider selection (fast for gating, heavy for complex tasks)
- Link resolver: cross-project MCP resource references (project/server:uri format)
- Prompt summary service: LLM-generated summaries and chapter extraction
- System project bootstrap: ensures default project exists on startup
- Structural link health checks: enrichWithLinkStatus on prompt GET endpoints
- CLI: create prompt --priority/--link, create project --gated/--no-gated,
describe project shows prompts section, get prompts shows PRI/LINK/STATUS
- Apply/edit: priority, linkTarget, gated fields supported
- Shell completions: fish updated with new flags
- 1,253 tests passing across all packages
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-25 23:22:42 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should allow deletion of non-system project prompts', async () => {
|
|
|
|
|
vi.mocked(promptRepo.findById).mockResolvedValue(makePrompt({ projectId: 'proj-1' }));
|
|
|
|
|
vi.mocked(projectRepo.findById).mockResolvedValue(makeProject({ id: 'proj-1', name: 'my-project' }));
|
|
|
|
|
|
|
|
|
|
await service.deletePrompt('prompt-1');
|
|
|
|
|
expect(promptRepo.delete).toHaveBeenCalledWith('prompt-1');
|
|
|
|
|
});
|
2026-02-24 14:53:00 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// ── PromptRequest CRUD ──
|
|
|
|
|
|
|
|
|
|
describe('listPromptRequests', () => {
|
|
|
|
|
it('should return all prompt requests', async () => {
|
|
|
|
|
const reqs = [makePromptRequest()];
|
|
|
|
|
vi.mocked(promptRequestRepo.findAll).mockResolvedValue(reqs);
|
|
|
|
|
|
|
|
|
|
const result = await service.listPromptRequests();
|
|
|
|
|
expect(result).toEqual(reqs);
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
describe('getPromptRequest', () => {
|
|
|
|
|
it('should return a prompt request by id', async () => {
|
|
|
|
|
const req = makePromptRequest();
|
|
|
|
|
vi.mocked(promptRequestRepo.findById).mockResolvedValue(req);
|
|
|
|
|
|
|
|
|
|
const result = await service.getPromptRequest('req-1');
|
|
|
|
|
expect(result).toEqual(req);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should throw for missing request', async () => {
|
|
|
|
|
await expect(service.getPromptRequest('nope')).rejects.toThrow('PromptRequest not found');
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
describe('deletePromptRequest', () => {
|
|
|
|
|
it('should delete an existing request', async () => {
|
|
|
|
|
vi.mocked(promptRequestRepo.findById).mockResolvedValue(makePromptRequest());
|
|
|
|
|
await service.deletePromptRequest('req-1');
|
|
|
|
|
expect(promptRequestRepo.delete).toHaveBeenCalledWith('req-1');
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// ── Propose ──
|
|
|
|
|
|
|
|
|
|
describe('propose', () => {
|
|
|
|
|
it('should create a prompt request', async () => {
|
|
|
|
|
const result = await service.propose({
|
|
|
|
|
name: 'my-prompt',
|
|
|
|
|
content: 'proposal',
|
|
|
|
|
createdBySession: 'sess-1',
|
|
|
|
|
});
|
|
|
|
|
expect(promptRequestRepo.create).toHaveBeenCalledWith(
|
|
|
|
|
expect.objectContaining({ name: 'my-prompt', content: 'proposal', createdBySession: 'sess-1' }),
|
|
|
|
|
);
|
|
|
|
|
expect(result.name).toBe('my-prompt');
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should validate project exists when projectId given', async () => {
|
|
|
|
|
vi.mocked(projectRepo.findById).mockResolvedValue(makeProject());
|
|
|
|
|
await service.propose({
|
|
|
|
|
name: 'scoped',
|
|
|
|
|
content: 'x',
|
|
|
|
|
projectId: 'proj-1',
|
|
|
|
|
});
|
|
|
|
|
expect(projectRepo.findById).toHaveBeenCalledWith('proj-1');
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// ── Approve ──
|
|
|
|
|
|
|
|
|
|
describe('approve', () => {
|
|
|
|
|
it('should delete request and create prompt (atomic)', async () => {
|
|
|
|
|
const req = makePromptRequest({ id: 'req-1', name: 'approved', content: 'good stuff', projectId: 'proj-1' });
|
|
|
|
|
vi.mocked(promptRequestRepo.findById).mockResolvedValue(req);
|
|
|
|
|
|
|
|
|
|
const result = await service.approve('req-1');
|
|
|
|
|
|
|
|
|
|
expect(promptRepo.create).toHaveBeenCalledWith(
|
|
|
|
|
expect.objectContaining({ name: 'approved', content: 'good stuff', projectId: 'proj-1' }),
|
|
|
|
|
);
|
|
|
|
|
expect(promptRequestRepo.delete).toHaveBeenCalledWith('req-1');
|
|
|
|
|
expect(result.name).toBe('approved');
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should throw for missing request', async () => {
|
|
|
|
|
await expect(service.approve('nope')).rejects.toThrow('PromptRequest not found');
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should handle global prompt (no projectId)', async () => {
|
|
|
|
|
const req = makePromptRequest({ id: 'req-2', name: 'global', content: 'stuff', projectId: null });
|
|
|
|
|
vi.mocked(promptRequestRepo.findById).mockResolvedValue(req);
|
|
|
|
|
|
|
|
|
|
await service.approve('req-2');
|
|
|
|
|
|
|
|
|
|
// Should NOT include projectId in the create call
|
|
|
|
|
const createArg = vi.mocked(promptRepo.create).mock.calls[0]![0];
|
|
|
|
|
expect(createArg).not.toHaveProperty('projectId');
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
feat: gated project experience & prompt intelligence
Implements the full gated session flow and prompt intelligence system:
- Prisma schema: add gated, priority, summary, chapters, linkTarget fields
- Session gate: state machine (gated → begin_session → ungated) with LLM-powered
tool selection based on prompt index
- Tag matcher: intelligent prompt-to-tool matching with project/server/action tags
- LLM selector: tiered provider selection (fast for gating, heavy for complex tasks)
- Link resolver: cross-project MCP resource references (project/server:uri format)
- Prompt summary service: LLM-generated summaries and chapter extraction
- System project bootstrap: ensures default project exists on startup
- Structural link health checks: enrichWithLinkStatus on prompt GET endpoints
- CLI: create prompt --priority/--link, create project --gated/--no-gated,
describe project shows prompts section, get prompts shows PRI/LINK/STATUS
- Apply/edit: priority, linkTarget, gated fields supported
- Shell completions: fish updated with new flags
- 1,253 tests passing across all packages
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-25 23:22:42 +00:00
|
|
|
// ── Priority ──
|
|
|
|
|
|
|
|
|
|
describe('prompt priority', () => {
|
|
|
|
|
it('creates prompt with explicit priority', async () => {
|
|
|
|
|
const result = await service.createPrompt({ name: 'high-pri', content: 'x', priority: 8 });
|
|
|
|
|
expect(promptRepo.create).toHaveBeenCalledWith(expect.objectContaining({ priority: 8 }));
|
|
|
|
|
expect(result.priority).toBe(8);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('uses default priority 5 when not specified', async () => {
|
|
|
|
|
const result = await service.createPrompt({ name: 'default-pri', content: 'x' });
|
|
|
|
|
// Default in schema is 5 — create is called without priority
|
|
|
|
|
const createArg = vi.mocked(promptRepo.create).mock.calls[0]![0];
|
|
|
|
|
expect(createArg.priority).toBeUndefined();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('rejects priority below 1', async () => {
|
|
|
|
|
await expect(
|
|
|
|
|
service.createPrompt({ name: 'bad-pri', content: 'x', priority: 0 }),
|
|
|
|
|
).rejects.toThrow();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('rejects priority above 10', async () => {
|
|
|
|
|
await expect(
|
|
|
|
|
service.createPrompt({ name: 'bad-pri', content: 'x', priority: 11 }),
|
|
|
|
|
).rejects.toThrow();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('updates prompt priority', async () => {
|
|
|
|
|
vi.mocked(promptRepo.findById).mockResolvedValue(makePrompt());
|
|
|
|
|
await service.updatePrompt('prompt-1', { priority: 3 });
|
|
|
|
|
expect(promptRepo.update).toHaveBeenCalledWith('prompt-1', expect.objectContaining({ priority: 3 }));
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// ── Link Target ──
|
|
|
|
|
|
|
|
|
|
describe('prompt links', () => {
|
|
|
|
|
it('creates linked prompt with valid linkTarget', async () => {
|
|
|
|
|
const result = await service.createPrompt({
|
|
|
|
|
name: 'linked',
|
|
|
|
|
content: 'link content',
|
|
|
|
|
linkTarget: 'other-project/docmost-mcp:docmost://pages/abc',
|
|
|
|
|
});
|
|
|
|
|
expect(promptRepo.create).toHaveBeenCalledWith(
|
|
|
|
|
expect.objectContaining({ linkTarget: 'other-project/docmost-mcp:docmost://pages/abc' }),
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('rejects invalid link format', async () => {
|
|
|
|
|
await expect(
|
|
|
|
|
service.createPrompt({ name: 'bad-link', content: 'x', linkTarget: 'invalid-format' }),
|
|
|
|
|
).rejects.toThrow();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('rejects link without server part', async () => {
|
|
|
|
|
await expect(
|
|
|
|
|
service.createPrompt({ name: 'bad-link', content: 'x', linkTarget: 'project:uri' }),
|
|
|
|
|
).rejects.toThrow();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('approve carries priority from request to prompt', async () => {
|
|
|
|
|
const req = makePromptRequest({ id: 'req-1', name: 'high-pri', content: 'x', projectId: 'proj-1', priority: 9 });
|
|
|
|
|
vi.mocked(promptRequestRepo.findById).mockResolvedValue(req);
|
|
|
|
|
|
|
|
|
|
await service.approve('req-1');
|
|
|
|
|
|
|
|
|
|
expect(promptRepo.create).toHaveBeenCalledWith(
|
|
|
|
|
expect.objectContaining({ priority: 9 }),
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('propose passes priority through', async () => {
|
|
|
|
|
const result = await service.propose({
|
|
|
|
|
name: 'pri-req',
|
|
|
|
|
content: 'x',
|
|
|
|
|
priority: 7,
|
|
|
|
|
});
|
|
|
|
|
expect(promptRequestRepo.create).toHaveBeenCalledWith(
|
|
|
|
|
expect.objectContaining({ priority: 7 }),
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
2026-02-24 14:53:00 +00:00
|
|
|
// ── Visibility ──
|
|
|
|
|
|
|
|
|
|
describe('getVisiblePrompts', () => {
|
|
|
|
|
it('should return approved prompts and session requests', async () => {
|
|
|
|
|
vi.mocked(promptRepo.findAll).mockResolvedValue([
|
|
|
|
|
makePrompt({ name: 'approved-1', content: 'A' }),
|
|
|
|
|
]);
|
|
|
|
|
vi.mocked(promptRequestRepo.findBySession).mockResolvedValue([
|
|
|
|
|
makePromptRequest({ name: 'pending-1', content: 'B' }),
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
const result = await service.getVisiblePrompts('proj-1', 'sess-1');
|
|
|
|
|
|
|
|
|
|
expect(result).toHaveLength(2);
|
2026-03-03 19:07:39 +00:00
|
|
|
expect(result[0]).toMatchObject({ name: 'approved-1', content: 'A', type: 'prompt' });
|
|
|
|
|
expect(result[1]).toMatchObject({ name: 'pending-1', content: 'B', type: 'promptrequest' });
|
2026-02-24 14:53:00 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should not include pending requests without sessionId', async () => {
|
|
|
|
|
vi.mocked(promptRepo.findAll).mockResolvedValue([makePrompt()]);
|
|
|
|
|
|
|
|
|
|
const result = await service.getVisiblePrompts('proj-1');
|
|
|
|
|
|
|
|
|
|
expect(result).toHaveLength(1);
|
|
|
|
|
expect(promptRequestRepo.findBySession).not.toHaveBeenCalled();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should return empty when no prompts or requests', async () => {
|
|
|
|
|
const result = await service.getVisiblePrompts();
|
|
|
|
|
expect(result).toEqual([]);
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
});
|