test: lock secrets capability contract

This commit is contained in:
mirivlad 2026-06-29 20:08:37 +08:00
parent b11178d8fd
commit f4f79cf1d0
7 changed files with 56 additions and 4 deletions

View File

@ -106,6 +106,7 @@ export interface VerstakPluginAPI {
readText(relativePath: string): Promise<string>;
readBytes(relativePath: string): Promise<FileBytes>;
writeText(relativePath: string, content: string, options?: WriteTextOptions): Promise<void>;
writeBytes(relativePath: string, dataBase64: string, options?: WriteTextOptions): Promise<void>;
createFolder(relativePath: string): Promise<void>;
move(fromRelativePath: string, toRelativePath: string, options?: MovePathOptions): Promise<void>;
trash(relativePath: string): Promise<TrashResult>;

File diff suppressed because one or more lines are too long

View File

@ -1 +1 @@
{"version":3,"file":"plugin-api.js","sourceRoot":"","sources":["../src/plugin-api.ts"],"names":[],"mappings":"AAAA,6DAA6D;AAC7D,EAAE;AACF,8EAA8E;AAC9E,gFAAgF;AAChF,8EAA8E;AAC9E,gDAAgD;AA0KhD,MAAM,UAAU,eAAe,CAAC,SAAiB;IAC/C,MAAM,IAAI,KAAK,CAAC,kEAAkE,CAAC,CAAC;AACtF,CAAC"}
{"version":3,"file":"plugin-api.js","sourceRoot":"","sources":["../src/plugin-api.ts"],"names":[],"mappings":"AAAA,6DAA6D;AAC7D,EAAE;AACF,8EAA8E;AAC9E,gFAAgF;AAChF,8EAA8E;AAC9E,gDAAgD;AA2KhD,MAAM,UAAU,eAAe,CAAC,SAAiB;IAC/C,MAAM,IAAI,KAAK,CAAC,kEAAkE,CAAC,CAAC;AACtF,CAAC"}

View File

@ -1 +1 @@
{"version":3,"file":"test-utils.d.ts","sourceRoot":"","sources":["../src/test-utils.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,cAAc,EAAE,WAAW,EAAE,4BAA4B,EAAE,MAAM,SAAS,CAAC;AACzF,OAAO,KAAK,EAAwB,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAQ3E,MAAM,WAAW,oBAAoB;IACnC,aAAa,CAAC,EAAE,4BAA4B,CAAC;CAC9C;AAED;;GAEG;AACH,wBAAgB,kBAAkB,CAAC,SAAS,CAAC,EAAE,OAAO,CAAC,cAAc,CAAC,GAAG,cAAc,CAetF;AAED;;GAEG;AACH,wBAAgB,qBAAqB,CAAC,SAAS,CAAC,EAAE,OAAO,CAAC,WAAW,CAAC,GAAG,WAAW,CASnF;AAED;;GAEG;AACH,wBAAgB,mBAAmB,CAAC,QAAQ,SAAgB,EAAE,OAAO,GAAE,oBAAyB,GAAG,gBAAgB,CA6TlH;AAED;;GAEG;AACH,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,OAAO,GAAG;IAAE,KAAK,EAAE,OAAO,CAAC;IAAC,MAAM,EAAE,MAAM,EAAE,CAAA;CAAE,CAgCxF;AAGD,OAAO,EAAE,EAAE,EAAE,MAAM,QAAQ,CAAC;AAC5B,OAAO,EAAE,EAAE,EAAE,CAAC"}
{"version":3,"file":"test-utils.d.ts","sourceRoot":"","sources":["../src/test-utils.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,cAAc,EAAE,WAAW,EAAE,4BAA4B,EAAE,MAAM,SAAS,CAAC;AACzF,OAAO,KAAK,EAAwB,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAQ3E,MAAM,WAAW,oBAAoB;IACnC,aAAa,CAAC,EAAE,4BAA4B,CAAC;CAC9C;AAED;;GAEG;AACH,wBAAgB,kBAAkB,CAAC,SAAS,CAAC,EAAE,OAAO,CAAC,cAAc,CAAC,GAAG,cAAc,CAetF;AAED;;GAEG;AACH,wBAAgB,qBAAqB,CAAC,SAAS,CAAC,EAAE,OAAO,CAAC,WAAW,CAAC,GAAG,WAAW,CASnF;AAED;;GAEG;AACH,wBAAgB,mBAAmB,CAAC,QAAQ,SAAgB,EAAE,OAAO,GAAE,oBAAyB,GAAG,gBAAgB,CA2VlH;AAED;;GAEG;AACH,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,OAAO,GAAG;IAAE,KAAK,EAAE,OAAO,CAAC;IAAC,MAAM,EAAE,MAAM,EAAE,CAAA;CAAE,CAgCxF;AAGD,OAAO,EAAE,EAAE,EAAE,MAAM,QAAQ,CAAC;AAC5B,OAAO,EAAE,EAAE,EAAE,CAAC"}

36
dist/test-utils.js vendored
View File

@ -88,6 +88,28 @@ export function createMockPluginAPI(pluginId = 'test.plugin', options = {}) {
}
return result;
}
function stringFromBase64(value) {
const alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
const clean = String(value || '').replace(/\s+/g, '');
if (clean.length % 4 === 1)
throw new Error('invalid-base64');
let result = '';
for (let i = 0; i < clean.length; i += 4) {
const a = alphabet.indexOf(clean[i]);
const b = alphabet.indexOf(clean[i + 1]);
const c = clean[i + 2] === '=' ? -1 : alphabet.indexOf(clean[i + 2]);
const d = clean[i + 3] === '=' ? -1 : alphabet.indexOf(clean[i + 3]);
if (a < 0 || b < 0 || (clean[i + 2] !== '=' && c < 0) || (clean[i + 3] !== '=' && d < 0)) {
throw new Error('invalid-base64');
}
result += String.fromCharCode((a << 2) | (b >> 4));
if (c >= 0)
result += String.fromCharCode(((b & 15) << 4) | (c >> 2));
if (d >= 0)
result += String.fromCharCode(((c & 3) << 6) | d);
}
return result;
}
function entry(path, node) {
const name = baseName(path);
const dot = name.lastIndexOf('.');
@ -248,6 +270,20 @@ export function createMockPluginAPI(pluginId = 'test.plugin', options = {}) {
throw new Error(`parent-not-found: ${parent}`);
files.set(path, { type: 'file', content, modifiedAt: new Date().toISOString() });
}),
writeBytes: vi.fn(async (relativePath, dataBase64, options = {}) => {
const path = normalizePath(relativePath);
const node = files.get(path);
if (node && node.type !== 'file')
throw new Error(`not-regular-file: ${path}`);
if (node && !options.overwrite)
throw new Error(`conflict: ${path}`);
if (!node && !options.createIfMissing)
throw new Error(`not-found: ${path}`);
const parent = parentPath(path);
if (!files.get(parent) || files.get(parent)?.type !== 'folder')
throw new Error(`parent-not-found: ${parent}`);
files.set(path, { type: 'file', content: stringFromBase64(dataBase64), modifiedAt: new Date().toISOString() });
}),
createFolder: vi.fn(async (relativePath) => {
const path = normalizePath(relativePath);
if (files.has(path))

File diff suppressed because one or more lines are too long

View File

@ -1,6 +1,8 @@
import { describe, expect, test } from 'vitest';
import { existsSync, readFileSync, readdirSync } from 'node:fs';
import capabilitiesSchema from '../schemas/capabilities.json';
import manifestSchema from '../schemas/manifest.json';
import permissionsSchema from '../schemas/permissions.json';
import vaultEventsSchema from '../schemas/events/vault.json';
import type { OpenResourceRequest, PluginManifest } from './types';
import { createMockPluginAPI } from './test-utils';
@ -53,6 +55,19 @@ describe('VerstakPluginAPI contract', () => {
expect(permissionEnum).toContain('workbench.open');
});
test('secrets capability and permissions are declared as dangerous platform contract', () => {
const capabilities = ((capabilitiesSchema as any).capabilities || []) as Array<{ name: string; status: string }>;
const permissions = ((permissionsSchema as any).permissions || []) as Array<{ name: string; dangerous: boolean }>;
const permissionEnum = ((manifestSchema as any).properties.permissions.items.enum || []) as string[];
expect(capabilities).toContainEqual(expect.objectContaining({ name: 'secret-store', status: 'draft' }));
expect(capabilities).toContainEqual(expect.objectContaining({ name: 'secrets.read-ui', status: 'draft' }));
expect(capabilities).toContainEqual(expect.objectContaining({ name: 'secrets.write-ui', status: 'draft' }));
expect(permissions).toContainEqual(expect.objectContaining({ name: 'secrets.read', dangerous: true }));
expect(permissions).toContainEqual(expect.objectContaining({ name: 'secrets.write', dangerous: true }));
expect(permissionEnum).toEqual(expect.arrayContaining(['secrets.read', 'secrets.write']));
});
test('file.changed schema documents watcher refresh payload', () => {
const fileChanged = (vaultEventsSchema as any).events.find((event: any) => event.name === 'file.changed');