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
37 changes: 21 additions & 16 deletions .claude/agents/test-writer.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ Write thorough Jest unit tests that match the project's existing patterns. When
**Test runner:** Jest 29 with `ts-jest`. Config in `jest.config.js`.

**Where tests live:**

- Tests always go in a `tests/` subfolder **inside the same directory as the source file**.
- `src/core/auth.ts` → `src/core/tests/auth.test.ts`
- `src/lib/assets/asset-utils.ts` → `src/lib/assets/tests/asset-utils.test.ts`
Expand All @@ -34,6 +35,7 @@ Write thorough Jest unit tests that match the project's existing patterns. When
The `jest.config.js` `testMatch` is `**/src/**/tests/**/*.test.ts` — any `tests/` folder under `src/` is automatically picked up.

**TypeScript path aliases** (pre-configured in `jest.config.js`):

- `core/*` → `src/core/*`
- `lib/*` → `src/lib/*`
- `types/*` → `src/types/*`
Expand All @@ -45,14 +47,14 @@ The `jest.config.js` `testMatch` is `**/src/**/tests/**/*.test.ts` — any `test
### Standard test file scaffold

```typescript
import { ThingToTest } from '../module-name';
import { resetState, setState, getState } from '../state';
import { ThingToTest } from "../module-name";
import { resetState, setState, getState } from "../state";

beforeEach(() => {
resetState();
jest.spyOn(console, 'log').mockImplementation(() => {});
jest.spyOn(console, 'warn').mockImplementation(() => {});
jest.spyOn(console, 'error').mockImplementation(() => {});
jest.spyOn(console, "log").mockImplementation(() => {});
jest.spyOn(console, "warn").mockImplementation(() => {});
jest.spyOn(console, "error").mockImplementation(() => {});
});

afterEach(() => {
Expand All @@ -65,15 +67,15 @@ Always suppress console output. Always call `resetState()` — the `state` objec
### When tests touch the filesystem

```typescript
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
import { setState } from '../state';
import * as fs from "fs";
import * as os from "os";
import * as path from "path";
import { setState } from "../state";

let tmpDir: string;

beforeAll(() => {
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'agility-test-'));
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "agility-test-"));
});

afterAll(() => {
Expand All @@ -94,16 +96,16 @@ Never write to `agility-files/` or the project root in tests. Always use `os.tmp
`getApiClient()` throws unless `state.mgmtApiOptions` or `state.token` is set. To unblock a constructor or method that calls it without testing the API:

```typescript
setState({ token: 'test-token', targetGuid: 'test-guid-u' });
setState({ token: "test-token", targetGuid: "test-guid-u" });
// This makes getApiClient() create a real (but unused) ApiClient from the SDK.
// Safe — the SDK constructor just stores options, makes no network calls.
```

For methods that actually *call* the API, mock `getApiClient`:
For methods that actually _call_ the API, mock `getApiClient`:

```typescript
jest.mock('../state', () => ({
...jest.requireActual('../state'),
jest.mock("../state", () => ({
...jest.requireActual("../state"),
getApiClient: jest.fn().mockReturnValue({
contentMethods: { saveContentItem: jest.fn().mockResolvedValue({ contentID: 99 }) },
// add only the methods your test needs
Expand All @@ -114,7 +116,7 @@ jest.mock('../state', () => ({
### When testing `fileOperations`

```typescript
const ops = new fileOperations('my-guid', 'en-us');
const ops = new fileOperations("my-guid", "en-us");
// instancePath = tmpDir/my-guid/en-us (because setState({ rootPath: tmpDir }))
```

Expand Down Expand Up @@ -147,6 +149,7 @@ const ops = new fileOperations('my-guid', 'en-us');
`state` is a single exported object. All functions share it. Always call `resetState()` in `beforeEach`.

Key defaults after `resetState()`:

- `sourceGuid: []`, `targetGuid: []`, `locale: []`
- `rootPath: 'agility-files'`
- `token: null`
Expand All @@ -159,6 +162,7 @@ Key defaults after `resetState()`:
### Auth URL routing (`src/core/auth.ts`)

`determineBaseUrl(guid)` and `determineFetchUrl(guid)` route by GUID suffix:

- `*u` → US (`mgmt.aglty.io` / `api.aglty.io`)
- `*c` → Canada, `*e` → EU, `*a` → AUS, `*d` → Dev, `*us2` → US2
- `state.local = true` → `https://localhost:5050` (management only, not fetch)
Expand All @@ -171,6 +175,7 @@ Pure utility: `createBatches<T>(items: T[], batchSize?: number): T[][]`. Default
### `fileOperations` (`src/core/fileOperations.ts`)

Path layout (normal mode):

- `instancePath` = `rootPath/guid/locale`
- `mappingsPath` = `rootPath/guid/mappings`
- Central mapping path = `rootPath/mappings/sourceGuid-targetGuid/locale/type/mappings.json`
Expand All @@ -197,7 +202,7 @@ Legacy mode (`state.legacyFolders = true`) flattens everything to `rootPath/`.
- Group tests with `describe` blocks by method/behavior. Match the naming style in existing tests.
- Use `it('does X when Y')` phrasing — describe behavior, not implementation.
- Use `it.each` for table-driven cases (multiple valid/invalid inputs, multiple enum values, etc.).
- Never add comments that describe what the code does — only write comments when the *why* is non-obvious.
- Never add comments that describe what the code does — only write comments when the _why_ is non-obvious.
- Don't import types you don't use.
- Keep each test focused on one assertion or one closely related group.
- After writing, always run `npm test` to confirm all tests pass before reporting done.
28 changes: 28 additions & 0 deletions .github/workflows/eslint.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
name: ESLint

on:
pull_request:
push:
branches:
- main

jobs:
lint:
name: ESLint Check
runs-on: ubuntu-latest

steps:
- name: Checkout
uses: actions/checkout@v4

- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: "24"
cache: "npm"

- name: Install dependencies
run: npm ci

- name: Run ESLint
run: npm run lint
2 changes: 1 addition & 1 deletion .github/workflows/npm-publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -48,4 +48,4 @@ jobs:
npm publish --access public --tag "$PRERELEASE_TAG"
else
npm publish --access public
fi
fi
21 changes: 21 additions & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# Build output and dependencies
dist/
node_modules/
coverage/

# Sync data — never reformat customer data
agility-files/

# Lockfiles
package-lock.json

# Generated logs
logs/
*.log

# OS / editor cruft
.DS_Store
.vscode-test/

# Backups
*.backup-*
10 changes: 8 additions & 2 deletions .prettierrc
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
{
"printWidth": 120
}
"printWidth": 120,
"tabWidth": 2,
"semi": true,
"singleQuote": false,
"trailingComma": "es5",
"arrowParens": "always",
"endOfLine": "lf"
}
3 changes: 3 additions & 0 deletions .vscode/extensions.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"recommendations": ["esbenp.prettier-vscode", "dbaeumer.vscode-eslint"]
}
26 changes: 26 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
{
"editor.formatOnSave": true,
"editor.defaultFormatter": "esbenp.prettier-vscode",
"[typescript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[javascript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[json]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[jsonc]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[yaml]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[markdown]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"prettier.requireConfig": true,
"files.eol": "\n",
"files.insertFinalNewline": true,
"files.trimTrailingWhitespace": true
}
Loading
Loading