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
6 changes: 6 additions & 0 deletions .github/workflows/functional-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,12 +29,18 @@ jobs:
- name: Checkout code
uses: actions/checkout@v5

- name: Submodules
run: git submodule update --init --recursive

- name: Setup Node.js
uses: volta-cli/action@v4

- name: Install dependencies
run: npm ci --no-audit

- name: Generate code
run: npm run codegen

- name: Build project
run: npm run build --if-present

Expand Down
6 changes: 6 additions & 0 deletions .github/workflows/nodejs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,18 @@ jobs:
- name: Checkout code
uses: actions/checkout@v5

- name: Submodules
run: git submodule update --init --recursive

- name: Setup Node.js
uses: volta-cli/action@v4

- name: Install dependencies
run: npm ci --no-audit

- name: Generate code
run: npm run codegen

- name: Run linting
run: npm run lint --if-present

Expand Down
27 changes: 24 additions & 3 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
on: workflow_dispatch
on:
release:
types: [published]

jobs:
publish:
Expand All @@ -10,11 +12,30 @@ jobs:
- uses: actions/checkout@v5
- uses: actions/setup-node@v5
with:
node-version: "22"
node-version: '22'

- name: Submodules
run: git submodule update --init --recursive

- name: Set version from tag
run: |
if [[ "$GITHUB_REF" == refs/tags/* ]]; then
TAG_VERSION=${GITHUB_REF#refs/tags/}
# Remove 'v' prefix if present
VERSION=${TAG_VERSION#v}
echo "Setting version to: $VERSION"
npm version "$VERSION"
else
echo "Not building from a tag, using default version"
fi

- run: npm run codegen

- run: npm ci

- run: npm run build:release
- uses: JS-DevTools/npm-publish@v3
with:
token: ${{ secrets.GITHUB_TOKEN }}
registry: "https://npm.pkg.github.com"
registry: 'https://npm.pkg.github.com'
access: restricted
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,5 @@ dist/
.env

.venv

src/generated
1 change: 1 addition & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
src/generated/
2 changes: 1 addition & 1 deletion .prettierrc
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,5 @@
"parser": "typescript",
"semi": true,
"singleQuote": true,
"trailingComma": "all"
"trailingComma": "all",
}
151 changes: 104 additions & 47 deletions __tests__/functional/main.functional.test.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,18 @@
import { describe, it, } from 'vitest';
import { ClassificationOutput, ClassifierSdk, type ClassifyImageInput, ImageFormat } from '../../src';
import { describe, it } from 'vitest';
import {
ClassificationOutput,
ClassifierSdk,
type ClassifyImageInput,
ImageFormat,
} from '../../src';
import fs from 'fs';
import { randomUUID } from 'crypto';

describe('ClassifierSdk Functional Tests', () => {
describe('listDeployments', () => {
it('should listDeployments and return responses (smoke test)', async ({ expect }) => {
it('should listDeployments and return responses (smoke test)', async ({
expect,
}) => {
// This is a smoke test. You must have a running gRPC server at localhost:50051 for this to pass.
// You may want to mock the gRPC client for true unit testing.
const sdk = new ClassifierSdk({
Expand All @@ -15,8 +22,8 @@ describe('ClassifierSdk Functional Tests', () => {
issuerUrl: process.env.VITE_OAUTH_ISSUER,
clientId: process.env.VITE_ATHENA_CLIENT_ID,
clientSecret: process.env.VITE_ATHENA_CLIENT_SECRET,
scope: 'manage:classify'
}
scope: 'manage:classify',
},
});

let error: any = null;
Expand All @@ -28,11 +35,41 @@ describe('ClassifierSdk Functional Tests', () => {
}
// Assert error is unset
expect(error).toBeNull();
}, 10000)
}, 10000);
});

describe('classifySingle', () => {
it('should classify a single image and return the response', async ({
expect,
}) => {
const imagePath = __dirname + '/448x448.jpg';
const sdk = new ClassifierSdk({
deploymentId: process.env.VITE_ATHENA_DEPLOYMENT_ID,
affiliate: process.env.VITE_ATHENA_AFFILIATE,
authentication: {
issuerUrl: process.env.VITE_OAUTH_ISSUER,
clientId: process.env.VITE_ATHENA_CLIENT_ID,
clientSecret: process.env.VITE_ATHENA_CLIENT_SECRET,
scope: 'manage:classify',
},
});

const input: ClassifyImageInput = {
data: fs.createReadStream(imagePath),
format: ImageFormat.PNG,
};

const response = await sdk.classifySingle(input);
expect(response.classifications).toBe(true);
expect(response.error).toBeNull();
}, 10000);
});

describe('classifyImage', () => {
it('should classify 10 images in a single request and return responses (integration smoke test)', async ({ expect, annotate }) => {
it('should classify 10 images in a single request and return responses (integration smoke test)', async ({
expect,
annotate,
}) => {
// This is a smoke test. You must have a running gRPC server at localhost:50051 for this to pass.
// You may want to mock the gRPC client for true unit testing.
const imagePath = __dirname + '/448x448.jpg';
Expand All @@ -43,23 +80,27 @@ describe('ClassifierSdk Functional Tests', () => {
issuerUrl: process.env.VITE_OAUTH_ISSUER,
clientId: process.env.VITE_ATHENA_CLIENT_ID,
clientSecret: process.env.VITE_ATHENA_CLIENT_SECRET,
scope: 'manage:classify'
}
scope: 'manage:classify',
},
});

// Generate 10 unique correlationIds
const correlationIds = Array.from({ length: 10 }, () => randomUUID().toString());
const correlationIds = Array.from({ length: 10 }, () =>
randomUUID().toString(),
);

correlationIds.sort((a, b) => a.localeCompare(b));

annotate(`Correlation IDs: ${correlationIds.join(', ')}`);

// Create 10 input objects, each with a new stream and unique correlationId
const inputs: ClassifyImageInput[] = correlationIds.map((correlationId) => ({
data: fs.createReadStream(imagePath),
format: ImageFormat.PNG,
correlationId
}));
const inputs: ClassifyImageInput[] = correlationIds.map(
(correlationId) => ({
data: fs.createReadStream(imagePath),
format: ImageFormat.PNG,
correlationId,
}),
);

// Create a promise to wrap the event emitter event 'data'
const promise = new Promise<ClassificationOutput[]>((resolve, reject) => {
Expand Down Expand Up @@ -107,23 +148,26 @@ describe('ClassifierSdk Functional Tests', () => {
// Check that all correlationIds are present in the outputs
expect(outputs.length).toBe(correlationIds.length);

const expectedOutputs = correlationIds.map(id => (
{
correlationId: id,
classifications: expect.toBeOneOf([expect.arrayContaining([
const expectedOutputs = correlationIds.map((id) => ({
correlationId: id,
classifications: expect.toBeOneOf([
expect.arrayContaining([
{
label: expect.any(String),
weight: expect.any(Number)
}
]), []])
}
));
weight: expect.any(Number),
},
]),
[],
]),
}));

expect(outputs).toMatchObject(expectedOutputs);
}, 120000);

it('should classify with raw uint8 resize return responses (integration smoke test)', async ({ expect, annotate }) => {

it('should classify with raw uint8 resize return responses (integration smoke test)', async ({
expect,
annotate,
}) => {
const imagePath = __dirname + '/448x448.jpg';
const sdk = new ClassifierSdk({
deploymentId: process.env.VITE_ATHENA_DEPLOYMENT_ID,
Expand All @@ -132,8 +176,8 @@ describe('ClassifierSdk Functional Tests', () => {
issuerUrl: process.env.VITE_OAUTH_ISSUER,
clientId: process.env.VITE_ATHENA_CLIENT_ID,
clientSecret: process.env.VITE_ATHENA_CLIENT_SECRET,
scope: 'manage:classify'
}
scope: 'manage:classify',
},
});

const correlationId = randomUUID();
Expand All @@ -148,7 +192,9 @@ describe('ClassifierSdk Functional Tests', () => {
}, 30000);

sdk.on('data', (data) => {
const byCorrelationId = data.outputs.filter(o => o.correlationId === correlationId);
const byCorrelationId = data.outputs.filter(
(o) => o.correlationId === correlationId,
);
if (byCorrelationId.length > 0) {
clearTimeout(timeout);
resolve(byCorrelationId);
Expand Down Expand Up @@ -185,20 +231,26 @@ describe('ClassifierSdk Functional Tests', () => {
expect(first).toMatchObject([
{
correlationId,
classifications: expect.toBeOneOf([expect.arrayContaining([
{
label: expect.any(String),
weight: expect.any(Number)
}
]), []])
} as ClassificationOutput
classifications: expect.toBeOneOf([
expect.arrayContaining([
{
label: expect.any(String),
weight: expect.any(Number),
},
]),
[],
]),
} as ClassificationOutput,
]);

// Accept either a successful call or a connection error (for CI/dev convenience)
expect(error).toBeUndefined();
}, 120000);

it('should classify return responses (integration smoke test)', async ({ expect, annotate }) => {
it('should classify return responses (integration smoke test)', async ({
expect,
annotate,
}) => {
// This is a smoke test. You must have a running gRPC server at localhost:50051 for this to pass.
// You may want to mock the gRPC client for true unit testing.
const imagePath = __dirname + '/448x448.jpg';
Expand All @@ -209,8 +261,8 @@ describe('ClassifierSdk Functional Tests', () => {
issuerUrl: process.env.VITE_OAUTH_ISSUER,
clientId: process.env.VITE_ATHENA_CLIENT_ID,
clientSecret: process.env.VITE_ATHENA_CLIENT_SECRET,
scope: 'manage:classify'
}
scope: 'manage:classify',
},
});

const correlationId = randomUUID();
Expand All @@ -225,7 +277,9 @@ describe('ClassifierSdk Functional Tests', () => {
}, 30000);

sdk.on('data', (data) => {
const byCorrelationId = data.outputs.filter(o => o.correlationId === correlationId);
const byCorrelationId = data.outputs.filter(
(o) => o.correlationId === correlationId,
);
if (byCorrelationId.length > 0) {
clearTimeout(timeout);
resolve(byCorrelationId);
Expand Down Expand Up @@ -262,13 +316,16 @@ describe('ClassifierSdk Functional Tests', () => {
expect(first).toMatchObject([
{
correlationId,
classifications: expect.toBeOneOf([expect.arrayContaining([
{
label: expect.any(String),
weight: expect.any(Number)
}
]), []])
} as ClassificationOutput
classifications: expect.toBeOneOf([
expect.arrayContaining([
{
label: expect.any(String),
weight: expect.any(Number),
},
]),
[],
]),
} as ClassificationOutput,
]);

// Accept either a successful call or a connection error (for CI/dev convenience)
Expand Down
4 changes: 4 additions & 0 deletions __tests__/unit/main.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,10 @@ describe('ClassifierSdk', () => {
expect(typeof sdk.sendClassifyRequest).toBe('function');
});

it('should have classifySingle method', () => {
expect(typeof sdk.classifySingle).toBe('function');
});

it('should throw error when sendClassifyRequest called without open', async () => {
const input = {
data: Buffer.from('test'),
Expand Down
1 change: 1 addition & 0 deletions eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export default tseslint.config(
'src/athena/**',
'athena-protobufs/**',
'docs/**',
'**/generated/**',
],
},
eslint.configs.recommended,
Expand Down
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@crispthinking/athena-classifier-sdk",
"version": "0.2.0",
"version": "0.0.0",
Comment thread
snus-kin marked this conversation as resolved.
"main": "./dist/src/index.js",
"description": "A Node.js SDK for the Athena classifier that uses gRPC transport.",
"type": "module",
Expand Down Expand Up @@ -50,7 +50,7 @@
"build": "tsc -p tsconfig.json",
"build:watch": "tsc -w -p tsconfig.json",
"build:release": "npm run clean && tsc -p tsconfig.release.json",
"codegen": "npx protoc --plugin=protoc-gen-ts=./node_modules/.bin/protoc-gen-ts --ts_out=import_style=module,client_grpc1:./src/athena --proto_path=./athena-protobufs/athena ./athena-protobufs/athena/athena.proto",
"codegen": "mkdir -p src/generated && npx protoc --plugin=protoc-gen-ts=./node_modules/.bin/protoc-gen-ts --ts_out=client_grpc1:./src/generated --proto_path=./athena-protobufs ./athena-protobufs/athena/**.proto",
"lint": "eslint .",
"lint:all": "npm run lint && npm run prettier:check && npx tsc --noEmit",
"precommit:setup": "echo 'To install pre-commit hooks, run: uv tool install pre-commit && uvx pre-commit install'",
Expand Down
Loading