Files
PaperClipAI/packages/google-sheets-mcp-server/src/google-client.test.ts
T
DottaandPaperclip 7b35de65aa feat(mcp) [split 1/8]: add fixture demo servers (#9556)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Governed MCP access spans contracts, runtime enforcement, adapters,
UI surfaces, and operator verification
> - The parity reference PR #9534 is too large for effective automated
or human review
> - The feature therefore needs a linear stack whose individual diffs
stay below the 100-file review limit
> - This pull request is split 1/8 and focuses on fixture and demo MCP
servers
> - The benefit is a standalone, testable review boundary while
preserving byte-for-byte parity at the top of the stack

## Linked Issues or Issue Description

- Related parity reference: #9534
- Problem: Developers need deterministic local MCP fixtures and visible
demo servers without pulling in the governed production runtime.
- Proposed solution: Adds the Google Sheets and KV demo MCP packages,
fixture catalog/servers, smoke harness, guide, and the root
smoke/typecheck registration hunks.
- Alternatives considered: keeping #9534 as one 403-file review, or
rewriting the feature to manufacture seams; both were rejected in favor
of path extraction plus compile-driven boundary moves.
- Roadmap alignment: this advances the existing governed MCP/tool-access
work already represented by #9534; it does not introduce a separate
roadmap initiative.
- Stack position: base branch is `master`.
- Merge policy: merge bottom-up, in order, only after the complete
eight-PR stack has been reviewed and the top-of-stack parity gate
remains empty.
- Requested review: QA for fixture and smoke coverage; Greptile on every
PR.

## What Changed

- Adds the Google Sheets and KV demo MCP packages, fixture
catalog/servers, smoke harness, guide, and the root smoke/typecheck
registration hunks.
- Keeps this PR below 100 changed files and independently typecheckable.
- Preserves the final tree from #9534 when combined with the other seven
stack levels.

## Verification

- `pnpm typecheck`
- `pnpm --filter @paperclipai/google-sheets-mcp-server test` — 27 tests
passed
- `pnpm --filter @paperclipai/kv-demo-mcp-server test` — 12 tests passed

## Risks

- The new packages add dependencies that are intentionally not committed
to `pnpm-lock.yaml`, per repository policy.
- Stack risk: merging out of order can expose incomplete layers;
mitigate by following the documented bottom-up merge policy.
- Parity risk: later edits to an intermediate branch can drift from
#9534; mitigate by re-running the empty top-of-stack diff before merge.

> For core feature work, check [`ROADMAP.md`](ROADMAP.md) first and
discuss it in `#dev` before opening the PR. Feature PRs that overlap
with planned core work may need to be redirected — check the roadmap
first. See `CONTRIBUTING.md`.

## Model Used

- OpenAI Codex, exact model ID `gpt-5.4`; runtime-managed context
window; medium reasoning with repository, shell, Git, GitHub CLI, and
code-execution tools enabled.

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have searched GitHub for duplicate or related PRs and linked
them above
- [x] I have either (a) linked existing issues with `Fixes: #` / `Closes
#` / `Refs #` OR (b) described the issue in-PR following the relevant
issue template
- [x] Internal references are omitted except the execution-plan link
explicitly required for this coordinated split stack
- [x] My branch name describes the change and contains no internal
Paperclip ticket id or instance-derived details
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [ ] All Paperclip CI gates are green
- [ ] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
- [x] I will address all Greptile and reviewer comments before
requesting merge


## Stack Coordination

- Internal execution plan:
[PAP-13874](/PAP/issues/PAP-13874#document-plan)
- Parity reference: #9534
- Stack: #9556 → #9557 → #9558 → #9559 → #9560 → #9561 → #9562 → #9563
- Merge bottom-up only after full-stack review and an empty parity diff
at #9563.

---------

Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-07-14 12:56:21 -05:00

145 lines
3.6 KiB
TypeScript

import { beforeEach, describe, expect, it, vi } from "vitest";
import { createGoogleSheetsClient } from "./google-client.js";
const mocks = vi.hoisted(() => ({
GoogleAuth: vi.fn(),
sheets: vi.fn(),
spreadsheetsGet: vi.fn(),
valuesGet: vi.fn(),
valuesAppend: vi.fn(),
valuesUpdate: vi.fn(),
valuesClear: vi.fn(),
batchUpdate: vi.fn(),
}));
vi.mock("googleapis", () => ({
google: {
auth: {
GoogleAuth: mocks.GoogleAuth,
},
sheets: mocks.sheets,
},
}));
const serviceAccount = {
client_email: "service@example.test",
private_key: "-----BEGIN PRIVATE KEY-----\nfake\n-----END PRIVATE KEY-----",
project_id: "project-1",
};
function resetGoogleapisMock() {
for (const mock of Object.values(mocks)) mock.mockReset();
mocks.GoogleAuth.mockImplementation(function GoogleAuth(this: { config?: unknown }, config: unknown) {
this.config = config;
});
mocks.sheets.mockReturnValue({
spreadsheets: {
get: mocks.spreadsheetsGet,
batchUpdate: mocks.batchUpdate,
values: {
get: mocks.valuesGet,
append: mocks.valuesAppend,
update: mocks.valuesUpdate,
clear: mocks.valuesClear,
},
},
});
}
describe("Google Sheets API client", () => {
beforeEach(() => {
resetGoogleapisMock();
});
it("creates a Sheets v4 client with service-account credentials", async () => {
mocks.valuesGet.mockResolvedValueOnce({
data: {
range: "Sheet1!A1:B2",
values: [["name", "amount"], ["paper", 12]],
},
});
const client = createGoogleSheetsClient(serviceAccount);
const values = await client.readValues("sheet-1", "Sheet1!A1:B2");
expect(mocks.GoogleAuth).toHaveBeenCalledWith({
credentials: serviceAccount,
scopes: ["https://www.googleapis.com/auth/spreadsheets"],
});
expect(mocks.sheets).toHaveBeenCalledWith({
version: "v4",
auth: { config: expect.any(Object) },
});
expect(mocks.valuesGet).toHaveBeenCalledWith({
spreadsheetId: "sheet-1",
range: "Sheet1!A1:B2",
});
expect(values.values).toEqual([["name", "amount"], ["paper", 12]]);
});
it("maps batchUpdate replies for add_sheet_tab and delete_rows", async () => {
mocks.batchUpdate
.mockResolvedValueOnce({
data: {
replies: [{
addSheet: {
properties: {
sheetId: 12,
title: "New",
index: 2,
gridProperties: { rowCount: 50, columnCount: 10 },
},
},
}],
},
})
.mockResolvedValueOnce({ data: {} });
const client = createGoogleSheetsClient(serviceAccount);
await expect(client.addSheetTab({
spreadsheetId: "sheet-1",
title: "New",
rowCount: 50,
columnCount: 10,
})).resolves.toEqual({
spreadsheetId: "sheet-1",
sheet: {
sheetId: 12,
title: "New",
index: 2,
rowCount: 50,
columnCount: 10,
},
});
await expect(client.deleteRows({
spreadsheetId: "sheet-1",
sheetId: 12,
startIndex: 3,
endIndex: 5,
})).resolves.toEqual({
spreadsheetId: "sheet-1",
sheetId: 12,
deletedRows: 2,
});
expect(mocks.batchUpdate).toHaveBeenCalledTimes(2);
expect(mocks.batchUpdate).toHaveBeenLastCalledWith({
spreadsheetId: "sheet-1",
requestBody: {
requests: [{
deleteDimension: {
range: {
sheetId: 12,
dimension: "ROWS",
startIndex: 3,
endIndex: 5,
},
},
}],
},
});
});
});