Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 6 additions & 6 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 6 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,11 @@
"zod": "^3.25.76"
},
"overrides": {
"fast-uri": "3.1.8"
"fast-uri": "3.1.8",
"glob": {
"minimatch": {
"brace-expansion": "2.1.7"
}
}
}
}
20 changes: 19 additions & 1 deletion src/tools/backup-tools.test.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,26 @@
import { describe, expect, it } from 'vitest';
import { restoreBackupTool } from './backup-tools.js';
import { z } from 'zod';
import { getBackupTool, listBackupsTool, restoreBackupTool } from './backup-tools.js';

describe('backup-tools', () => {
it('does not expose a restore option for full backup restore', () => {
expect(restoreBackupTool.inputSchema).not.toHaveProperty('restoreOption');
});

it('accepts ISO 8601 enforcedRetentionEndTime in backup output schemas', () => {
const backup = {
name: 'projects/p1/locations/us-central1/backupVaults/bv1/backups/b1',
backupId: 'b1',
backupVaultId: 'bv1',
state: 'READY',
sourceVolume: 'projects/p1/locations/us-central1/volumes/vol1',
enforcedRetentionEndTime: '2009-02-13T23:31:30.000Z',
};

const getSchema = z.object(getBackupTool.outputSchema);
const listSchema = z.object(listBackupsTool.outputSchema);

expect(() => getSchema.parse(backup)).not.toThrow();
expect(() => listSchema.parse({ backups: [backup] })).not.toThrow();
});
});
16 changes: 8 additions & 8 deletions src/tools/backup-tools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,19 +52,19 @@ export const getBackupTool: ToolConfig = {
backupVaultId: z.string().describe('The ID of the backup vault containing the backup'),
state: z.string().describe('The current state of the backup'),
description: z.string().optional().describe('Description of the backup'),
volumeUsagebytes: z.number().optional().describe('The size of the volume in bytes'),
volumeUsagebytes: z.string().optional().describe('The size of the volume in bytes'),
backupType: z.string().optional().describe('The type of the backup'),
sourceVolume: z.string().describe('The name of the source volume'),
createTime: z.string().optional().describe('The creation time of the backup'),
chainStoragebytes: z.number().optional().describe('The size of the backup chain in bytes'),
chainStoragebytes: z.string().optional().describe('The size of the backup chain in bytes'),
satisfiesPzs: z.boolean().optional().describe('Whether the backup satisfies PZS requirements'),
satisfiesPzi: z.boolean().optional().describe('Whether the backup satisfies PZI requirements'),
volumeRegion: z.string().optional().describe('The region of the source volume'),
backupRegion: z.string().optional().describe('The region where the backup is stored'),
enforcedRetentionEndTime: z
.number()
.string()
.optional()
.describe('The number of days the backup is retained'),
.describe('The time until which the backup is not deletable (ISO 8601)'),
sourceSnapshot: z
.string()
.optional()
Expand Down Expand Up @@ -98,12 +98,12 @@ export const listBackupsTool: ToolConfig = {
backupVaultId: z.string().describe('The ID of the backup vault containing the backup'),
state: z.string().describe('The current state of the backup'),
description: z.string().optional().describe('Description of the backup'),
volumeUsagebytes: z.number().optional().describe('The size of the volume in bytes'),
volumeUsagebytes: z.string().optional().describe('The size of the volume in bytes'),
backupType: z.string().optional().describe('The type of the backup'),
sourceVolume: z.string().describe('The name of the source volume'),
createTime: z.string().optional().describe('The creation time of the backup'),
chainStoragebytes: z
.number()
.string()
.optional()
.describe('The size of the backup chain in bytes'),
satisfiesPzs: z
Expand All @@ -117,9 +117,9 @@ export const listBackupsTool: ToolConfig = {
volumeRegion: z.string().optional().describe('The region of the source volume'),
backupRegion: z.string().optional().describe('The region where the backup is stored'),
enforcedRetentionEndTime: z
.number()
.string()
.optional()
.describe('The number of days the backup is retained'),
.describe('The time until which the backup is not deletable (ISO 8601)'),
sourceSnapshot: z
.string()
.optional()
Expand Down
156 changes: 149 additions & 7 deletions src/tools/handlers/backup-handler.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { z } from 'zod';
import { getBackupTool, listBackupsTool } from '../backup-tools.js';

const createClientMock = vi.fn();

Expand Down Expand Up @@ -135,9 +137,6 @@ describe('backup-handler', () => {
backupVaultId: 'bv1',
state: 'UNKNOWN',
});
expect((result.structuredContent as any).sourceVolume).toContain(
'projects/p1/locations/us-central1/'
);
});

it('getBackupHandler formats sourceVolume and createTime when present', async () => {
Expand Down Expand Up @@ -175,12 +174,12 @@ describe('backup-handler', () => {
createTime: { seconds: 1 },
description: 'd',
backupType: 'MANUAL',
chainStoragebytes: 0,
chainStoragebytes: '0',
satisfiesPzs: false,
satisfiesPzi: false,
volumeRegion: 'r1',
backupRegion: 'r2',
enforcedRetentionEndTime: 't',
enforcedRetentionEndTime: { seconds: 1234567890, nanos: 500_000_000 },
sourceSnapshot: 'snap',
labels: { a: 'b' },
},
Expand All @@ -202,12 +201,12 @@ describe('backup-handler', () => {
state: 'READY',
description: 'd',
backupType: 'MANUAL',
chainStoragebytes: 0,
chainStoragebytes: '0',
satisfiesPzs: false,
satisfiesPzi: false,
volumeRegion: 'r1',
backupRegion: 'r2',
enforcedRetentionEndTime: 't',
enforcedRetentionEndTime: '2009-02-13T23:31:30.500Z',
sourceSnapshot: 'snap',
labels: { a: 'b' },
});
Expand Down Expand Up @@ -255,6 +254,149 @@ describe('backup-handler', () => {
}
});

it('listBackupsHandler preserves protobuf Timestamp nanos in enforcedRetentionEndTime', async () => {
const listBackups = vi.fn().mockResolvedValue([
[
{
name: 'projects/p1/locations/us-central1/backupVaults/bv1/backups/b1',
sourceVolume: 'projects/p1/locations/us-central1/volumes/vol1',
state: 'READY',
enforcedRetentionEndTime: { seconds: 1234567890, nanos: 500_000_000 },
},
],
undefined,
undefined,
]);
createClientMock.mockReturnValue({ listBackups });

const { listBackupsHandler } = await import('./backup-handler.js');
const result = await listBackupsHandler({
projectId: 'p1',
location: 'us-central1',
backupVaultId: 'bv1',
});

expect((result.structuredContent as any).backups[0].enforcedRetentionEndTime).toBe(
'2009-02-13T23:31:30.500Z'
);
});

it('listBackupsHandler preserves backupId when name is absent', async () => {
const listBackups = vi.fn().mockResolvedValue([
[
{
backupId: 'b-sparse',
state: 'READY',
sourceVolume: 'projects/p1/locations/us-central1/volumes/vol1',
},
],
undefined,
undefined,
]);
createClientMock.mockReturnValue({ listBackups });

const { listBackupsHandler } = await import('./backup-handler.js');
const result = await listBackupsHandler({
projectId: 'p1',
location: 'us-central1',
backupVaultId: 'bv1',
});

expect((result.structuredContent as any).backups[0]).toMatchObject({
backupId: 'b-sparse',
state: 'READY',
});
});

it('listBackupsHandler preserves string int64 byte fields', async () => {
const listBackups = vi.fn().mockResolvedValue([
[
{
name: 'projects/p1/locations/us-central1/backupVaults/bv1/backups/b1',
sourceVolume: 'projects/p1/locations/us-central1/volumes/vol1',
state: 'READY',
volumeUsagebytes: '12345',
chainStoragebytes: '67890',
},
],
undefined,
undefined,
]);
createClientMock.mockReturnValue({ listBackups });

const { listBackupsHandler } = await import('./backup-handler.js');
const result = await listBackupsHandler({
projectId: 'p1',
location: 'us-central1',
backupVaultId: 'bv1',
});

expect((result.structuredContent as any).backups[0]).toMatchObject({
volumeUsagebytes: '12345',
chainStoragebytes: '67890',
});
});

it('listBackupsHandler preserves int64 byte fields above MAX_SAFE_INTEGER', async () => {
const listBackups = vi.fn().mockResolvedValue([
[
{
name: 'projects/p1/locations/us-central1/backupVaults/bv1/backups/b1',
sourceVolume: 'projects/p1/locations/us-central1/volumes/vol1',
state: 'READY',
volumeUsagebytes: '9007199254740993',
chainStoragebytes: '9007199254740993',
},
],
undefined,
undefined,
]);
createClientMock.mockReturnValue({ listBackups });

const { listBackupsHandler } = await import('./backup-handler.js');
const result = await listBackupsHandler({
projectId: 'p1',
location: 'us-central1',
backupVaultId: 'bv1',
});

expect((result.structuredContent as any).backups[0]).toMatchObject({
volumeUsagebytes: '9007199254740993',
chainStoragebytes: '9007199254740993',
});
});

it('listBackupsHandler structuredContent passes MCP output schema validation', async () => {
const listBackups = vi.fn().mockResolvedValue([
[
{
name: 'projects/p1/locations/us-central1/backupVaults/bv1/backups/b1',
sourceVolume: 'projects/p1/locations/us-central1/volumes/vol1',
state: 'READY',
createTime: { seconds: 1234567890, nanos: 0 },
enforcedRetentionEndTime: { seconds: 1234567890, nanos: 500_000_000 },
volumeUsagebytes: '100',
},
],
undefined,
undefined,
]);
createClientMock.mockReturnValue({ listBackups });

const listOutputSchema = z.object(listBackupsTool.outputSchema);
const { listBackupsHandler } = await import('./backup-handler.js');
const result = await listBackupsHandler({
projectId: 'p1',
location: 'us-central1',
backupVaultId: 'bv1',
});

expect(() => listOutputSchema.parse(result.structuredContent)).not.toThrow();
expect(() =>
z.object(getBackupTool.outputSchema).parse(result.structuredContent.backups[0])
).not.toThrow();
});

it('listBackupsHandler calls listBackups and returns formatted backups + nextPageToken', async () => {
const listBackups = vi
.fn()
Expand Down
Loading
Loading