feat(v2.7): 建立协作治理基础合同

This commit is contained in:
2026-07-08 16:09:45 +08:00
parent 27cc1badc7
commit ad36ffda17
8 changed files with 448 additions and 12 deletions

View File

@@ -0,0 +1,51 @@
import { AuditService } from './audit.service';
describe('AuditService V2.7 adapter contract', () => {
it('writes an append-only audit event with actor and resource scope', async () => {
const prisma = {
auditLog: {
create: jest.fn().mockResolvedValue({ id: 'audit-1' }),
},
};
const service = new AuditService(prisma as any);
await service.record({
actorId: 'm-admin',
action: 'project_member.role_changed',
resourceType: 'project_member',
resourceId: 'pm-1',
projectId: 'project-1',
before: { role: 'member' },
after: { role: 'admin' },
});
expect(prisma.auditLog.create).toHaveBeenCalledWith({
data: expect.objectContaining({
actorId: 'm-admin',
action: 'project_member.role_changed',
resourceType: 'project_member',
resourceId: 'pm-1',
projectId: 'project-1',
before: { role: 'member' },
after: { role: 'admin' },
}),
});
});
it('does not fail the business action when audit storage is unavailable', async () => {
const prisma = {
auditLog: {
create: jest.fn().mockRejectedValue(new Error('database unavailable')),
},
};
const service = new AuditService(prisma as any);
(service as any).logger.warn = jest.fn();
await expect(service.record({
actorId: 'm-admin',
action: 'comment.created',
resourceType: 'comment',
resourceId: 'comment-1',
})).resolves.toBeUndefined();
});
});

View File

@@ -0,0 +1,43 @@
import { Injectable, Logger } from '@nestjs/common';
import { PrismaService } from '../../prisma/prisma.service';
export interface AuditEventInput {
actorId?: string | null;
action: string;
resourceType: string;
resourceId: string;
productId?: string | null;
projectId?: string | null;
versionId?: string | null;
before?: unknown;
after?: unknown;
metadata?: unknown;
}
@Injectable()
export class AuditService {
private readonly logger = new Logger(AuditService.name);
constructor(private readonly prisma: PrismaService) {}
async record(input: AuditEventInput): Promise<void> {
try {
await this.prisma.auditLog.create({
data: {
actorId: input.actorId ?? null,
action: input.action,
resourceType: input.resourceType,
resourceId: input.resourceId,
productId: input.productId ?? null,
projectId: input.projectId ?? null,
versionId: input.versionId ?? null,
before: input.before ?? {},
after: input.after ?? {},
metadata: input.metadata ?? {},
},
});
} catch (error: any) {
this.logger.warn(`Audit write failed for ${input.action}: ${error?.message ?? error}`);
}
}
}