sync: update from private repo (ce93095)
CI / build-and-test (push) Has been cancelled

This commit is contained in:
oss-sync
2026-06-10 03:52:37 +00:00
parent eb35e32f7a
commit 9f8958c4a2
27 changed files with 261 additions and 1582 deletions
+46 -470
View File
@@ -81,9 +81,7 @@ beforeEach(() => {
mkdirSync(userFolderRoot, { recursive: true });
// Create user subdirs (including memory and trash for memory tool tests)
mkdirSync(join(userFolderRoot, TEST_USER, 'scripts'), { recursive: true });
mkdirSync(join(userFolderRoot, TEST_USER, 'browser-macros'), { recursive: true });
mkdirSync(join(userFolderRoot, TEST_USER, 'templates'), { recursive: true });
mkdirSync(join(userFolderRoot, TEST_USER, 'recordings'), { recursive: true });
mkdirSync(join(userFolderRoot, TEST_USER, 'memory'), { recursive: true });
mkdirSync(join(userFolderRoot, TEST_USER, 'trash'), { recursive: true });
@@ -117,12 +115,12 @@ describe('TOOL_DEFS', () => {
// ── ListUserAssets ────────────────────────────────────────────────────────────
describe('ListUserAssets', () => {
it('lists scripts with descriptions and params', async () => {
writeFileSync(join(userFolderRoot, TEST_USER, 'scripts', 'foo.js'), SCRIPT_FOO);
writeFileSync(join(userFolderRoot, TEST_USER, 'scripts', 'bar.js'), SCRIPT_BAR);
it('lists browser-macros with descriptions and params', async () => {
writeFileSync(join(userFolderRoot, TEST_USER, 'browser-macros', 'foo.js'), SCRIPT_FOO);
writeFileSync(join(userFolderRoot, TEST_USER, 'browser-macros', 'bar.js'), SCRIPT_BAR);
const ctx = buildCtx({ userId: TEST_USER });
const result = await executeTool('ListUserAssets', { kind: 'scripts' }, ctx);
const result = await executeTool('ListUserAssets', { kind: 'browser-macros' }, ctx);
expect(result).not.toBeNull();
expect(result!.isError).toBe(false);
@@ -131,7 +129,7 @@ describe('ListUserAssets', () => {
expect(result!.output).toContain('date:string');
expect(result!.output).toContain('bar.js');
expect(result!.output).toContain('Check dashboard');
expect(result!.output).toContain('Scripts (2)');
expect(result!.output).toContain('Browser Macros (2)');
});
it('returns error when userId is missing', async () => {
@@ -145,41 +143,43 @@ describe('ListUserAssets', () => {
it('cross-user access is denied (ctx.userId must match target folder)', async () => {
// Create another user's folder
mkdirSync(join(userFolderRoot, 'other-user', 'scripts'), { recursive: true });
mkdirSync(join(userFolderRoot, 'other-user', 'browser-macros'), { recursive: true });
writeFileSync(
join(userFolderRoot, 'other-user', 'scripts', 'secret.js'),
join(userFolderRoot, 'other-user', 'browser-macros', 'secret.js'),
SCRIPT_FOO,
);
// Logged in as TEST_USER but the tool always reads from ctx.userId,
// so there is no way to list another user's folder.
// Verify that the tool reads only TEST_USER's scripts (0 scripts).
// Verify that the tool reads only TEST_USER's macros (0 macros).
const ctx = buildCtx({ userId: TEST_USER });
const result = await executeTool('ListUserAssets', { kind: 'scripts' }, ctx);
const result = await executeTool('ListUserAssets', { kind: 'browser-macros' }, ctx);
expect(result!.isError).toBe(false);
expect(result!.output).toContain('Scripts (0)');
expect(result!.output).toContain('Browser Macros (0)');
expect(result!.output).not.toContain('secret.js');
});
it('returns "all" categories when kind is omitted', async () => {
writeFileSync(join(userFolderRoot, TEST_USER, 'scripts', 'foo.js'), SCRIPT_FOO);
writeFileSync(join(userFolderRoot, TEST_USER, 'browser-macros', 'foo.js'), SCRIPT_FOO);
const ctx = buildCtx({ userId: TEST_USER });
const result = await executeTool('ListUserAssets', {}, ctx);
expect(result!.isError).toBe(false);
expect(result!.output).toContain('Scripts');
expect(result!.output).toContain('Templates');
expect(result!.output).toContain('Browser Macros');
expect(result!.output).toContain('Recordings');
// Retired categories must no longer appear
expect(result!.output).not.toContain('Templates');
expect(result!.output).not.toContain('Scripts (');
});
});
// ── RunUserScript ─────────────────────────────────────────────────────────────
describe('RunUserScript', () => {
it('runs a fixture script that returns a value', async () => {
writeFileSync(join(userFolderRoot, TEST_USER, 'scripts', 'nofm.js'), SCRIPT_NO_FM);
it('runs a fixture macro that returns a value', async () => {
writeFileSync(join(userFolderRoot, TEST_USER, 'browser-macros', 'nofm.js'), SCRIPT_NO_FM);
const ctx = buildCtx({ userId: TEST_USER });
const result = await executeTool('RunUserScript', { name: 'nofm' }, ctx);
@@ -190,8 +190,8 @@ describe('RunUserScript', () => {
});
it('returns isError true with "param" in message for bad params', async () => {
// Script declares `date:string` but we pass wrong type
writeFileSync(join(userFolderRoot, TEST_USER, 'scripts', 'foo.js'), SCRIPT_FOO);
// Macro declares `date:string` but we pass wrong type
writeFileSync(join(userFolderRoot, TEST_USER, 'browser-macros', 'foo.js'), SCRIPT_FOO);
const ctx = buildCtx({ userId: TEST_USER });
const result = await executeTool(
@@ -289,67 +289,34 @@ module.exports = async function main() { return 'ok'; };
expect(result!.output).toMatch(/invalid script name|outside owner folder|not found/);
});
// ── P2a: kind fallback (undefined → search scripts/ then browser-macros/) ─
// ── Retirement (2026-06): plain-Node scripts/ are gone ─────────────────────
it('resolves to scripts/ first when kind omitted', async () => {
writeFileSync(join(userFolderRoot, TEST_USER, 'scripts', 'nofm.js'), SCRIPT_NO_FM);
writeFileSync(join(userFolderRoot, TEST_USER, 'browser-macros', 'nofm.js'), SCRIPT_THROWS);
it('a file living only in the retired scripts/ dir is no longer resolvable', async () => {
mkdirSync(join(userFolderRoot, TEST_USER, 'scripts'), { recursive: true });
writeFileSync(join(userFolderRoot, TEST_USER, 'scripts', 'legacy.js'), SCRIPT_NO_FM);
const ctx = buildCtx({ userId: TEST_USER });
// kind omitted → should find scripts/nofm.js (plain runtime, returns 42)
const result = await executeTool('RunUserScript', { name: 'nofm' }, ctx);
expect(result!.isError).toBe(false);
expect(result!.output).toContain('42');
const result = await executeTool('RunUserScript', { name: 'legacy' }, ctx);
expect(result!.isError).toBe(true);
expect(result!.output.toLowerCase()).toContain('not found');
});
it('falls back to browser-macros/ when kind omitted and not in scripts/', async () => {
// Only exists in browser-macros/
it('explicit kind: "script" is rejected with a pointer to Bash/Skills', async () => {
const ctx = buildCtx({ userId: TEST_USER });
const result = await executeTool('RunUserScript', { name: 'whatever', kind: 'script' }, ctx);
expect(result!.isError).toBe(true);
expect(result!.output).toContain('retired');
expect(result!.output).toContain('Bash');
});
it('kind: "browser-macro" still accepted explicitly (back-compat)', async () => {
writeFileSync(join(userFolderRoot, TEST_USER, 'browser-macros', 'nofm.js'), SCRIPT_NO_FM);
const ctx = buildCtx({ userId: TEST_USER });
const result = await executeTool('RunUserScript', { name: 'nofm' }, ctx);
// Should find it in browser-macros/ (plain-compatible SCRIPT_NO_FM, no playwright deps)
expect(result!.isError).toBe(false);
expect(result!.output).toContain('42');
});
it('kind: "browser-macro" goes straight to browser-macros/', async () => {
writeFileSync(join(userFolderRoot, TEST_USER, 'scripts', 'nofm.js'), SCRIPT_THROWS);
writeFileSync(join(userFolderRoot, TEST_USER, 'browser-macros', 'nofm.js'), SCRIPT_NO_FM);
const ctx = buildCtx({ userId: TEST_USER });
// Explicit kind=browser-macro should use browser-macros/ not scripts/
const result = await executeTool('RunUserScript', { name: 'nofm', kind: 'browser-macro' }, ctx);
expect(result!.isError).toBe(false);
expect(result!.output).toContain('42');
});
it('returns error mentioning both subdirs when kind omitted and not found', async () => {
const ctx = buildCtx({ userId: TEST_USER });
const result = await executeTool('RunUserScript', { name: 'nonexistent' }, ctx);
expect(result!.isError).toBe(true);
expect(result!.output).toContain('scripts/');
expect(result!.output).toContain('browser-macros/');
});
// ── session_profile_id in scripts/ (plain runtime) → friendly error ───────
it('rejects scripts/*.js with session_profile_id (plain runtime mismatch)', async () => {
const scriptWithSession = `\
---
description: Mistakenly placed browser macro
session_profile_id: 3
---
module.exports = async function main() { return 'unreachable'; };
`;
writeFileSync(join(userFolderRoot, TEST_USER, 'scripts', 'wrongplace.js'), scriptWithSession);
const ctx = buildCtx({ userId: TEST_USER });
const result = await executeTool('RunUserScript', { name: 'wrongplace', kind: 'script' }, ctx);
expect(result!.isError).toBe(true);
expect(result!.output).toMatch(/session_profile_id/);
expect(result!.output).toMatch(/browser-macros/);
});
});
// ── UpdateUserMemory ──────────────────────────────────────────────────────────
@@ -447,232 +414,6 @@ describe('UpdateUserMemory', () => {
});
});
// ── ReadUserTemplate ──────────────────────────────────────────────────────────
describe('ReadUserTemplate', () => {
it('reads a plain markdown template by name', async () => {
writeFileSync(
join(userFolderRoot, TEST_USER, 'templates', 'weekly-report.md'),
'# Weekly Report\n\nFill in this week\'s highlights here.\n',
);
const ctx = buildCtx({ userId: TEST_USER });
const result = await executeTool('ReadUserTemplate', { name: 'weekly-report' }, ctx);
expect(result).not.toBeNull();
expect(result!.isError).toBe(false);
expect(result!.output).toContain('# Template: weekly-report');
expect(result!.output).toContain('Fill in this week\'s highlights here.');
});
it('reads a template with frontmatter', async () => {
const content = [
'---',
'title: API Error Email',
'audience: external',
'---',
'Dear customer,',
'',
'We apologize for the inconvenience.',
].join('\n') + '\n';
writeFileSync(
join(userFolderRoot, TEST_USER, 'templates', 'api-error-email.md'),
content,
);
const ctx = buildCtx({ userId: TEST_USER });
const result = await executeTool('ReadUserTemplate', { name: 'api-error-email' }, ctx);
expect(result!.isError).toBe(false);
expect(result!.output).toContain('## Frontmatter');
expect(result!.output).toContain('title');
expect(result!.output).toContain('API Error Email');
expect(result!.output).toContain('Dear customer,');
});
it('returns error for missing template', async () => {
const ctx = buildCtx({ userId: TEST_USER });
const result = await executeTool('ReadUserTemplate', { name: 'nonexistent' }, ctx);
expect(result!.isError).toBe(true);
expect(result!.output).toContain('not found');
});
it('rejects path traversal in name', async () => {
const ctx = buildCtx({ userId: TEST_USER });
const result = await executeTool('ReadUserTemplate', { name: '../escape' }, ctx);
expect(result!.isError).toBe(true);
});
it('handles name with .md suffix gracefully', async () => {
writeFileSync(
join(userFolderRoot, TEST_USER, 'templates', 'boilerplate.md'),
'Hello world template.\n',
);
const ctx = buildCtx({ userId: TEST_USER });
// Pass name with .md extension — should still work
const result = await executeTool('ReadUserTemplate', { name: 'boilerplate.md' }, ctx);
expect(result!.isError).toBe(false);
expect(result!.output).toContain('Hello world template.');
});
it('rejects without authenticated user', async () => {
const ctx = buildCtx({ userId: undefined });
const result = await executeTool('ReadUserTemplate', { name: 'anything' }, ctx);
expect(result!.isError).toBe(true);
expect(result!.output).toContain('authenticated');
});
});
// ── RenderUserTemplate ────────────────────────────────────────────────────────
describe('RenderUserTemplate', () => {
it('substitutes {{var}} declared in frontmatter.params', async () => {
const content = [
'---',
'description: Weekly report',
'params:',
' - name: date',
' type: string',
' - name: summary',
' type: string',
'---',
'On {{date}}: {{summary}}',
].join('\n') + '\n';
writeFileSync(join(userFolderRoot, TEST_USER, 'templates', 'weekly.md'), content);
const ctx = buildCtx({ userId: TEST_USER });
const result = await executeTool(
'RenderUserTemplate',
{ name: 'weekly', params: { date: '2026-05-11', summary: 'shipped 3 PRs' } },
ctx,
);
expect(result!.isError).toBe(false);
expect(result!.output).toBe('On 2026-05-11: shipped 3 PRs\n');
});
it('applies declared defaults when params are omitted', async () => {
const content = [
'---',
'description: Greeting',
'params:',
' - name: name',
' type: string',
' default: world',
'---',
'Hello, {{name}}!',
].join('\n') + '\n';
writeFileSync(join(userFolderRoot, TEST_USER, 'templates', 'greet.md'), content);
const ctx = buildCtx({ userId: TEST_USER });
const result = await executeTool('RenderUserTemplate', { name: 'greet' }, ctx);
expect(result!.isError).toBe(false);
expect(result!.output).toBe('Hello, world!\n');
});
it('leaves undeclared {{var}} literal', async () => {
const content = [
'---',
'description: Mixed',
'params:',
' - name: title',
' type: string',
'---',
'# {{title}}\n\nSee {{see_also}} for details.',
].join('\n') + '\n';
writeFileSync(join(userFolderRoot, TEST_USER, 'templates', 'mixed.md'), content);
const ctx = buildCtx({ userId: TEST_USER });
const result = await executeTool(
'RenderUserTemplate',
{ name: 'mixed', params: { title: 'Notes' } },
ctx,
);
expect(result!.isError).toBe(false);
expect(result!.output).toContain('# Notes');
expect(result!.output).toContain('See {{see_also}} for details.');
});
it('rejects when a required param is missing', async () => {
const content = [
'---',
'description: Required param',
'params:',
' - name: subject',
' type: string',
'---',
'Subject: {{subject}}',
].join('\n') + '\n';
writeFileSync(join(userFolderRoot, TEST_USER, 'templates', 'req.md'), content);
const ctx = buildCtx({ userId: TEST_USER });
const result = await executeTool('RenderUserTemplate', { name: 'req' }, ctx);
expect(result!.isError).toBe(true);
expect(result!.output).toMatch(/subject.*required/i);
});
it('rejects type-mismatched params', async () => {
const content = [
'---',
'description: Number param',
'params:',
' - name: count',
' type: number',
'---',
'Total: {{count}}',
].join('\n') + '\n';
writeFileSync(join(userFolderRoot, TEST_USER, 'templates', 'count.md'), content);
const ctx = buildCtx({ userId: TEST_USER });
const result = await executeTool(
'RenderUserTemplate',
{ name: 'count', params: { count: 'three' } },
ctx,
);
expect(result!.isError).toBe(true);
expect(result!.output).toMatch(/count.*expected number/);
});
it('renders a template without frontmatter as-is', async () => {
writeFileSync(
join(userFolderRoot, TEST_USER, 'templates', 'plain.md'),
'Just plain text with {{notRendered}}.\n',
);
const ctx = buildCtx({ userId: TEST_USER });
const result = await executeTool('RenderUserTemplate', { name: 'plain' }, ctx);
expect(result!.isError).toBe(false);
expect(result!.output).toBe('Just plain text with {{notRendered}}.\n');
});
it('rejects path traversal', async () => {
const ctx = buildCtx({ userId: TEST_USER });
const result = await executeTool('RenderUserTemplate', { name: '../escape' }, ctx);
expect(result!.isError).toBe(true);
});
it('returns error for missing template', async () => {
const ctx = buildCtx({ userId: TEST_USER });
const result = await executeTool('RenderUserTemplate', { name: 'never-existed' }, ctx);
expect(result!.isError).toBe(true);
expect(result!.output).toContain('not found');
});
it('rejects without authenticated user', async () => {
const ctx = buildCtx({ userId: undefined });
const result = await executeTool('RenderUserTemplate', { name: 'anything' }, ctx);
expect(result!.isError).toBe(true);
expect(result!.output).toContain('authenticated');
});
});
// ── ReadUserMemory ────────────────────────────────────────────────────────────
describe('ReadUserMemory', () => {
@@ -725,7 +466,7 @@ describe('RunUserScript: tools.user_scripts_allow_userids', () => {
tools: { userScriptsEnabled: true, userScriptsAllowUserids: ['other-user'] },
});
writeFileSync(
join(userFolderRoot, TEST_USER, 'scripts', 'noop.js'),
join(userFolderRoot, TEST_USER, 'browser-macros', 'noop.js'),
`---\nparams: []\n---\nasync function main(){return 'ok';}\nmodule.exports=main;\n`,
'utf-8',
);
@@ -740,7 +481,7 @@ describe('RunUserScript: tools.user_scripts_allow_userids', () => {
tools: { userScriptsEnabled: true, userScriptsAllowUserids: [TEST_USER, 'someone-else'] },
});
writeFileSync(
join(userFolderRoot, TEST_USER, 'scripts', 'ok.js'),
join(userFolderRoot, TEST_USER, 'browser-macros', 'ok.js'),
`---\nparams: []\n---\nasync function main(){return 'allowed';}\nmodule.exports=main;\n`,
'utf-8',
);
@@ -755,7 +496,7 @@ describe('RunUserScript: tools.user_scripts_allow_userids', () => {
tools: { userScriptsEnabled: true, userScriptsAllowUserids: [] },
});
writeFileSync(
join(userFolderRoot, TEST_USER, 'scripts', 'empty.js'),
join(userFolderRoot, TEST_USER, 'browser-macros', 'empty.js'),
`---\nparams: []\n---\nasync function main(){return 'still ok';}\nmodule.exports=main;\n`,
'utf-8',
);
@@ -792,7 +533,7 @@ async function main({ context, params }) {
`;
describe('WriteUserScript', () => {
it('writes a plain script to scripts/', async () => {
it('rejects the retired kind: "script" with a pointer to Skills/Bash', async () => {
const ctx = buildCtx({ userId: TEST_USER });
const result = await executeTool('WriteUserScript', {
name: 'fetch-and-clean',
@@ -801,13 +542,8 @@ describe('WriteUserScript', () => {
}, ctx);
expect(result).not.toBeNull();
expect(result!.isError).toBe(false);
expect(result!.output).toContain('scripts/fetch-and-clean.js');
const written = join(userFolderRoot, TEST_USER, 'scripts', 'fetch-and-clean.js');
const { existsSync: checkExists, readFileSync: rf } = await import('fs');
expect(checkExists(written)).toBe(true);
expect(rf(written, 'utf-8')).toBe(VALID_SCRIPT);
expect(result!.isError).toBe(true);
expect(result!.output).toContain('retired');
});
it('writes a browser-macro to browser-macros/', async () => {
@@ -826,7 +562,7 @@ describe('WriteUserScript', () => {
expect(checkExists(written)).toBe(true);
});
it('defaults kind to "script" when omitted', async () => {
it('writes to browser-macros/ when kind is omitted', async () => {
const ctx = buildCtx({ userId: TEST_USER });
const result = await executeTool('WriteUserScript', {
name: 'implicit-kind',
@@ -834,7 +570,7 @@ describe('WriteUserScript', () => {
}, ctx);
expect(result!.isError).toBe(false);
expect(result!.output).toContain('scripts/implicit-kind.js');
expect(result!.output).toContain('browser-macros/implicit-kind.js');
});
it('accepts name with .js suffix (strips it)', async () => {
@@ -845,7 +581,7 @@ describe('WriteUserScript', () => {
}, ctx);
expect(result!.isError).toBe(false);
expect(result!.output).toContain('scripts/my-script.js');
expect(result!.output).toContain('browser-macros/my-script.js');
// Must not write "my-script.js.js"
expect(result!.output).not.toContain('my-script.js.js');
});
@@ -946,7 +682,7 @@ describe('WriteUserScript', () => {
expect(result!.isError).toBe(false);
const { readFileSync: rf } = await import('fs');
const written = join(userFolderRoot, TEST_USER, 'scripts', 'overwritable.js');
const written = join(userFolderRoot, TEST_USER, 'browser-macros', 'overwritable.js');
expect(rf(written, 'utf-8')).toContain('updated');
});
@@ -984,166 +720,6 @@ describe('WriteUserScript', () => {
});
});
// ── WriteUserTemplate ─────────────────────────────────────────────────────────
const VALID_TEMPLATE = `\
---
description: Weekly report
params:
- name: date
type: string
---
# Weekly Report — {{date}}
Highlights this week:
- ...
`;
describe('WriteUserTemplate', () => {
it('writes a template to templates/', async () => {
const ctx = buildCtx({ userId: TEST_USER });
const result = await executeTool('WriteUserTemplate', {
name: 'weekly-report',
content: VALID_TEMPLATE,
}, ctx);
expect(result).not.toBeNull();
expect(result!.isError).toBe(false);
expect(result!.output).toContain('templates/weekly-report.md');
const { existsSync: checkExists, readFileSync: rf } = await import('fs');
const written = join(userFolderRoot, TEST_USER, 'templates', 'weekly-report.md');
expect(checkExists(written)).toBe(true);
expect(rf(written, 'utf-8')).toBe(VALID_TEMPLATE);
});
it('accepts name with .md suffix (strips it)', async () => {
const ctx = buildCtx({ userId: TEST_USER });
const result = await executeTool('WriteUserTemplate', {
name: 'report.md',
content: VALID_TEMPLATE,
}, ctx);
expect(result!.isError).toBe(false);
expect(result!.output).toContain('templates/report.md');
// Must not write "report.md.md"
expect(result!.output).not.toContain('report.md.md');
});
it('rejects missing name', async () => {
const ctx = buildCtx({ userId: TEST_USER });
const result = await executeTool('WriteUserTemplate', { content: VALID_TEMPLATE }, ctx);
expect(result!.isError).toBe(true);
expect(result!.output).toContain('"name"');
});
it('rejects non-slug name (slash)', async () => {
const ctx = buildCtx({ userId: TEST_USER });
const result = await executeTool('WriteUserTemplate', {
name: 'foo/bar',
content: VALID_TEMPLATE,
}, ctx);
expect(result!.isError).toBe(true);
expect(result!.output.toLowerCase()).toContain('alphanumeric');
});
it('rejects path traversal via name', async () => {
const ctx = buildCtx({ userId: TEST_USER });
const result = await executeTool('WriteUserTemplate', {
name: '../escape',
content: VALID_TEMPLATE,
}, ctx);
expect(result!.isError).toBe(true);
});
it('rejects oversized content', async () => {
const ctx = buildCtx({ userId: TEST_USER });
const huge = '# template\n' + 'x'.repeat(128 * 1024);
const result = await executeTool('WriteUserTemplate', {
name: 'big-template',
content: huge,
}, ctx);
expect(result!.isError).toBe(true);
expect(result!.output).toContain('bytes');
});
it('rejects duplicate without overwrite', async () => {
const ctx = buildCtx({ userId: TEST_USER });
await executeTool('WriteUserTemplate', { name: 'dup', content: VALID_TEMPLATE }, ctx);
const result = await executeTool('WriteUserTemplate', { name: 'dup', content: VALID_TEMPLATE }, ctx);
expect(result!.isError).toBe(true);
expect(result!.output).toContain('overwrite');
});
it('overwrites when overwrite: true', async () => {
const ctx = buildCtx({ userId: TEST_USER });
await executeTool('WriteUserTemplate', { name: 'overwritable-tmpl', content: VALID_TEMPLATE }, ctx);
const updated = VALID_TEMPLATE.replace('Weekly Report', 'Monthly Report');
const result = await executeTool('WriteUserTemplate', {
name: 'overwritable-tmpl',
content: updated,
overwrite: true,
}, ctx);
expect(result!.isError).toBe(false);
const { readFileSync: rf } = await import('fs');
const written = join(userFolderRoot, TEST_USER, 'templates', 'overwritable-tmpl.md');
expect(rf(written, 'utf-8')).toContain('Monthly Report');
});
it('requires authenticated user', async () => {
const ctx = buildCtx({ userId: undefined });
const result = await executeTool('WriteUserTemplate', {
name: 'test',
content: VALID_TEMPLATE,
}, ctx);
expect(result!.isError).toBe(true);
expect(result!.output).toContain('authenticated');
});
it('invokes auditLog on success', async () => {
const auditCalls: Array<{ action: string; detail: Record<string, unknown> }> = [];
setUserFolderToolDeps({
sessRepo: null as never,
masterKeyPath: '',
userFolderRoot,
auditLog: (action, detail) => { auditCalls.push({ action, detail: detail as Record<string, unknown> }); },
});
const ctx = buildCtx({ userId: TEST_USER });
const result = await executeTool('WriteUserTemplate', {
name: 'audit-tmpl',
content: VALID_TEMPLATE,
}, ctx);
expect(result!.isError).toBe(false);
const entry = auditCalls.find(c => c.action === 'user_template_written');
expect(entry).toBeDefined();
expect(entry!.detail['userId']).toBe(TEST_USER);
expect(entry!.detail['filename']).toBe('audit-tmpl.md');
});
it('written template is immediately readable via ReadUserTemplate', async () => {
const ctx = buildCtx({ userId: TEST_USER });
await executeTool('WriteUserTemplate', {
name: 'round-trip',
content: VALID_TEMPLATE,
}, ctx);
const result = await executeTool('ReadUserTemplate', { name: 'round-trip' }, ctx);
expect(result!.isError).toBe(false);
expect(result!.output).toContain('Weekly Report');
expect(result!.output).toContain('{{date}}');
});
});
describe('RunUserScript: audit log hook', () => {
afterEach(() => {
mockedLoadConfig.mockReturnValue({ tools: { userScriptsEnabled: true } });
@@ -1160,7 +736,7 @@ describe('RunUserScript: audit log hook', () => {
},
});
writeFileSync(
join(userFolderRoot, TEST_USER, 'scripts', 'audited.js'),
join(userFolderRoot, TEST_USER, 'browser-macros', 'audited.js'),
`---\nparams: []\n---\nasync function main(){return 'audited ok';}\nmodule.exports=main;\n`,
'utf-8',
);