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
118 changes: 118 additions & 0 deletions .agents/skills/migrate-to-shoehorn/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
---
name: migrate-to-shoehorn
description: Migrate test files from `as` type assertions to @total-typescript/shoehorn. Use when user mentions shoehorn, wants to replace `as` in tests, or needs partial test data.
---

# Migrate to Shoehorn

## Why shoehorn?

`shoehorn` lets you pass partial data in tests while keeping TypeScript happy. It replaces `as` assertions with type-safe alternatives.

**Test code only.** Never use shoehorn in production code.

Problems with `as` in tests:

- Trained not to use it
- Must manually specify target type
- Double-as (`as unknown as Type`) for intentionally wrong data

## Install

```bash
npm i @total-typescript/shoehorn
```

## Migration patterns

### Large objects with few needed properties

Before:

```ts
type Request = {
body: { id: string };
headers: Record<string, string>;
cookies: Record<string, string>;
// ...20 more properties
};

it("gets user by id", () => {
// Only care about body.id but must fake entire Request
getUser({
body: { id: "123" },
headers: {},
cookies: {},
// ...fake all 20 properties
});
});
```

After:

```ts
import { fromPartial } from "@total-typescript/shoehorn";

it("gets user by id", () => {
getUser(
fromPartial({
body: { id: "123" },
}),
);
});
```

### `as Type` → `fromPartial()`

Before:

```ts
getUser({ body: { id: "123" } } as Request);
```

After:

```ts
import { fromPartial } from "@total-typescript/shoehorn";

getUser(fromPartial({ body: { id: "123" } }));
```

### `as unknown as Type` → `fromAny()`

Before:

```ts
getUser({ body: { id: 123 } } as unknown as Request); // wrong type on purpose
```

After:

```ts
import { fromAny } from "@total-typescript/shoehorn";

getUser(fromAny({ body: { id: 123 } }));
```

## When to use each

| Function | Use case |
| --------------- | -------------------------------------------------- |
| `fromPartial()` | Pass partial data that still type-checks |
| `fromAny()` | Pass intentionally wrong data (keeps autocomplete) |
| `fromExact()` | Force full object (swap with fromPartial later) |

## Workflow

1. **Gather requirements** - ask user:
- What test files have `as` assertions causing problems?
- Are they dealing with large objects where only some properties matter?
- Do they need to pass intentionally wrong data for error testing?

2. **Install and migrate**:
- [ ] Install: `npm i @total-typescript/shoehorn`
- [ ] Find test files with `as` assertions: `grep -r " as [A-Z]" --include="*.test.ts" --include="*.spec.ts"`
- [ ] Replace `as Type` with `fromPartial()`
- [ ] Replace `as unknown as Type` with `fromAny()`
- [ ] Add imports from `@total-typescript/shoehorn`
- [ ] Run type check to verify
Comment on lines +1 to +118

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🔴 Critical

Fix skillPath mismatch in skills-lock.json.

The skills-lock.json entry for migrate-to-shoehorn declares skillPath as "skills/misc/migrate-to-shoehorn/SKILL.md", but this file is actually located at .agents/skills/migrate-to-shoehorn/SKILL.md. This path mismatch means the lock entry does not correctly resolve to the skill documentation. Update the skillPath in skills-lock.json to match the actual file location.

🧰 Tools
🪛 SkillSpector (2.3.7)

[warning] 99: [MP2] Context Window Stuffing: Skill attempts to fill the context window with filler content, displacing legitimate instructions and safety constraints. This can degrade agent performance or bypass safety boundaries.

Remediation: Implement context-window management that detects and rejects padding or stuffing attempts. Prioritize system instructions over user-injected content.

(Memory Poisoning (MP2))

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.agents/skills/migrate-to-shoehorn/SKILL.md around lines 1 - 118, The
`migrate-to-shoehorn` lock entry has a `skillPath` that does not match the
actual `SKILL.md` location, so update the `skills-lock.json` entry to point to
the `.agents/skills/migrate-to-shoehorn/SKILL.md` path; use the
`migrate-to-shoehorn` skill name in `skills-lock.json` to find the affected
record and keep the path consistent with the documented skill file location.

1 change: 1 addition & 0 deletions apps/api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
"@cortex/typescript-config": "workspace:*",
"@nestjs/cli": "^11.0.21",
"@nestjs/testing": "^11.1.24",
"@total-typescript/shoehorn": "^0.1.2",
"@types/express": "^5.0.3",
"@types/jest": "^30.0.0",
"@types/jsonwebtoken": "^9.0.10",
Expand Down
4 changes: 2 additions & 2 deletions apps/api/src/mcp/identity.service.spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { fromAny } from "@total-typescript/shoehorn";
import { IdentityService } from "./identity.service";
import type { PrismaService } from "../prisma/prisma.service";

/** Minimal mock shape for PrismaService covering only what IdentityService uses. */
function makeMockPrisma() {
Expand All @@ -16,7 +16,7 @@ describe("IdentityService", () => {

beforeEach(() => {
prisma = makeMockPrisma();
service = new IdentityService(prisma as unknown as PrismaService);
service = new IdentityService(fromAny(prisma));
});

afterEach(() => {
Expand Down
13 changes: 8 additions & 5 deletions apps/api/src/mcp/mcp.controller.spec.ts
Original file line number Diff line number Diff line change
@@ -1,30 +1,33 @@
import { fromAny, fromPartial } from "@total-typescript/shoehorn";
import type { Request } from "express";
import { MCPController } from "./mcp.controller";
import { IdentityService } from "./identity.service";
import type { AuthenticatedUser } from "../auth/auth.types";

type RequestWithJwtUser = Request & { user: AuthenticatedUser };

const mockUser: AuthenticatedUser = {
id: "user_123",
email: "alice@example.com",
organizationId: "org_456",
role: "member",
};

function makeReq(user: AuthenticatedUser = mockUser) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return { user } as any;
function makeReq(user: AuthenticatedUser = mockUser): RequestWithJwtUser {
return fromAny({ user });
}

describe("MCPController", () => {
let controller: MCPController;
let identityService: jest.Mocked<IdentityService>;

beforeEach(() => {
identityService = {
identityService = fromPartial<jest.Mocked<IdentityService>>({
resolveScope: jest.fn().mockResolvedValue({
organizationId: "org_456",
departmentId: "dept_789",
}),
} as unknown as jest.Mocked<IdentityService>;
});

controller = new MCPController(identityService);
});
Expand Down
1 change: 1 addition & 0 deletions packages/shared/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
"license": "ISC",
"devDependencies": {
"@cortex/typescript-config": "workspace:*",
"@total-typescript/shoehorn": "^0.1.2",
"@types/jest": "^30.0.0",
"@types/node": "^22.0.0",
"jest": "^30.4.2",
Expand Down
11 changes: 11 additions & 0 deletions pnpm-lock.yaml

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

6 changes: 6 additions & 0 deletions skills-lock.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
{
"version": 1,
"skills": {
"migrate-to-shoehorn": {
"source": "mattpocock/skills",
"sourceType": "github",
"skillPath": "skills/misc/migrate-to-shoehorn/SKILL.md",
"computedHash": "67fdd18f8f4f7c89b3003eb944220679272738c1deb680719fd2e282495d87f4"
},
"next-best-practices": {
"source": "vercel-labs/next-skills",
"sourceType": "github",
Expand Down
Loading