diff --git a/sdk/csharp/SynorCompute.Tests/SynorCompute.Tests.csproj b/sdk/csharp/SynorCompute.Tests/SynorCompute.Tests.csproj
new file mode 100644
index 0000000..f3e222e
--- /dev/null
+++ b/sdk/csharp/SynorCompute.Tests/SynorCompute.Tests.csproj
@@ -0,0 +1,23 @@
+
+
+
+ net8.0
+ enable
+ enable
+ false
+
+
+
+
+
+
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+ all
+
+
+
+
+
+
+
+
diff --git a/sdk/csharp/SynorCompute.Tests/TensorTests.cs b/sdk/csharp/SynorCompute.Tests/TensorTests.cs
new file mode 100644
index 0000000..01f302d
--- /dev/null
+++ b/sdk/csharp/SynorCompute.Tests/TensorTests.cs
@@ -0,0 +1,198 @@
+using Xunit;
+using SynorCompute;
+
+namespace SynorCompute.Tests;
+
+public class TensorTests
+{
+ [Fact]
+ public void Constructor_WithShapeAndData_CreatesTensor()
+ {
+ var tensor = new Tensor([2, 3], [1.0, 2.0, 3.0, 4.0, 5.0, 6.0]);
+
+ Assert.Equal([2, 3], tensor.Shape);
+ Assert.Equal(6, tensor.Size);
+ Assert.Equal(2, tensor.Ndim);
+ }
+
+ [Fact]
+ public void Zeros_CreatesZeroTensor()
+ {
+ var tensor = Tensor.Zeros([3, 4]);
+
+ Assert.Equal([3, 4], tensor.Shape);
+ Assert.Equal(12, tensor.Size);
+ Assert.All(tensor.Data, v => Assert.Equal(0.0, v));
+ }
+
+ [Fact]
+ public void Ones_CreatesOnesTensor()
+ {
+ var tensor = Tensor.Ones([2, 2]);
+
+ Assert.All(tensor.Data, v => Assert.Equal(1.0, v));
+ }
+
+ [Fact]
+ public void Random_CreatesRandomTensor()
+ {
+ var tensor = Tensor.Random([10, 10]);
+
+ Assert.Equal([10, 10], tensor.Shape);
+ Assert.All(tensor.Data, v => Assert.InRange(v, 0.0, 1.0));
+ }
+
+ [Fact]
+ public void Randn_CreatesNormalDistributionTensor()
+ {
+ var tensor = Tensor.Randn([1000]);
+
+ var mean = tensor.Mean();
+ var std = tensor.Std();
+
+ Assert.InRange(Math.Abs(mean), 0, 0.2);
+ Assert.InRange(std, 0.8, 1.2);
+ }
+
+ [Fact]
+ public void Eye_CreatesIdentityMatrix()
+ {
+ var tensor = Tensor.Eye(3);
+
+ Assert.Equal([3, 3], tensor.Shape);
+ Assert.Equal(1.0, tensor[0, 0]);
+ Assert.Equal(1.0, tensor[1, 1]);
+ Assert.Equal(1.0, tensor[2, 2]);
+ Assert.Equal(0.0, tensor[0, 1]);
+ }
+
+ [Fact]
+ public void Arange_CreatesRangeTensor()
+ {
+ var tensor = Tensor.Arange(0, 5, 1);
+
+ Assert.Equal([5], tensor.Shape);
+ Assert.Equal(0.0, tensor[0]);
+ Assert.Equal(4.0, tensor[4]);
+ }
+
+ [Fact]
+ public void Linspace_CreatesLinearlySpacedTensor()
+ {
+ var tensor = Tensor.Linspace(0, 10, 11);
+
+ Assert.Equal([11], tensor.Shape);
+ Assert.Equal(0.0, tensor[0], 4);
+ Assert.Equal(10.0, tensor[10], 4);
+ Assert.Equal(5.0, tensor[5], 4);
+ }
+
+ [Fact]
+ public void Reshape_ChangesShape()
+ {
+ var tensor = new Tensor([6], [1, 2, 3, 4, 5, 6]);
+ var reshaped = tensor.Reshape([2, 3]);
+
+ Assert.Equal([2, 3], reshaped.Shape);
+ Assert.Equal(6, reshaped.Size);
+ }
+
+ [Fact]
+ public void Reshape_ThrowsOnInvalidShape()
+ {
+ var tensor = Tensor.Zeros([4]);
+
+ Assert.Throws(() => tensor.Reshape([2, 3]));
+ }
+
+ [Fact]
+ public void Transpose_Transposes2DTensor()
+ {
+ var tensor = new Tensor([2, 3], [1, 2, 3, 4, 5, 6]);
+ var transposed = tensor.Transpose();
+
+ Assert.Equal([3, 2], transposed.Shape);
+ Assert.Equal(1.0, transposed[0, 0]);
+ Assert.Equal(4.0, transposed[0, 1]);
+ }
+
+ [Fact]
+ public void Sum_CalculatesSum()
+ {
+ var tensor = new Tensor([4], [1, 2, 3, 4]);
+
+ Assert.Equal(10.0, tensor.Sum());
+ }
+
+ [Fact]
+ public void Mean_CalculatesMean()
+ {
+ var tensor = new Tensor([4], [1, 2, 3, 4]);
+
+ Assert.Equal(2.5, tensor.Mean());
+ }
+
+ [Fact]
+ public void Std_CalculatesStandardDeviation()
+ {
+ var tensor = new Tensor([4], [1, 2, 3, 4]);
+
+ Assert.Equal(1.118, tensor.Std(), 3);
+ }
+
+ [Fact]
+ public void Min_FindsMinimum()
+ {
+ var tensor = new Tensor([4], [3, 1, 4, 2]);
+
+ Assert.Equal(1.0, tensor.Min());
+ }
+
+ [Fact]
+ public void Max_FindsMaximum()
+ {
+ var tensor = new Tensor([4], [3, 1, 4, 2]);
+
+ Assert.Equal(4.0, tensor.Max());
+ }
+
+ [Fact]
+ public void ReLU_AppliesActivation()
+ {
+ var tensor = new Tensor([5], [-2, -1, 0, 1, 2]);
+ var result = tensor.ReLU();
+
+ Assert.Equal([0.0, 0.0, 0.0, 1.0, 2.0], result.Data);
+ }
+
+ [Fact]
+ public void Sigmoid_AppliesActivation()
+ {
+ var tensor = new Tensor([1], [0]);
+ var result = tensor.Sigmoid();
+
+ Assert.Equal(0.5, result[0], 4);
+ }
+
+ [Fact]
+ public void Softmax_AppliesActivation()
+ {
+ var tensor = new Tensor([3], [1, 2, 3]);
+ var result = tensor.Softmax();
+
+ Assert.Equal(1.0, result.Sum(), 4);
+ Assert.True(result[2] > result[1]);
+ Assert.True(result[1] > result[0]);
+ }
+
+ [Fact]
+ public void Serialization_RoundTrip()
+ {
+ var original = new Tensor([2, 3], [1, 2, 3, 4, 5, 6]);
+ var json = original.ToJson();
+ var restored = Tensor.FromJson(json);
+
+ Assert.Equal(original.Shape, restored.Shape);
+ Assert.Equal(original.Data, restored.Data);
+ }
+}
diff --git a/sdk/csharp/SynorCompute.Tests/TypesTests.cs b/sdk/csharp/SynorCompute.Tests/TypesTests.cs
new file mode 100644
index 0000000..e4d105c
--- /dev/null
+++ b/sdk/csharp/SynorCompute.Tests/TypesTests.cs
@@ -0,0 +1,192 @@
+using Xunit;
+using SynorCompute;
+
+namespace SynorCompute.Tests;
+
+public class TypesTests
+{
+ [Fact]
+ public void ProcessorType_HasAllValues()
+ {
+ Assert.Equal("cpu", ProcessorType.Cpu.ToValue());
+ Assert.Equal("gpu", ProcessorType.Gpu.ToValue());
+ Assert.Equal("tpu", ProcessorType.Tpu.ToValue());
+ Assert.Equal("npu", ProcessorType.Npu.ToValue());
+ Assert.Equal("lpu", ProcessorType.Lpu.ToValue());
+ Assert.Equal("auto", ProcessorType.Auto.ToValue());
+ }
+
+ [Fact]
+ public void ProcessorType_ParsesFromString()
+ {
+ Assert.Equal(ProcessorType.Gpu, ProcessorTypeExtensions.FromString("gpu"));
+ Assert.Equal(ProcessorType.Tpu, ProcessorTypeExtensions.FromString("tpu"));
+ Assert.Equal(ProcessorType.Auto, ProcessorTypeExtensions.FromString("invalid"));
+ }
+
+ [Fact]
+ public void Precision_HasAllValues()
+ {
+ Assert.Equal("fp64", Precision.FP64.ToValue());
+ Assert.Equal("fp32", Precision.FP32.ToValue());
+ Assert.Equal("fp16", Precision.FP16.ToValue());
+ Assert.Equal("bf16", Precision.BF16.ToValue());
+ Assert.Equal("int8", Precision.INT8.ToValue());
+ Assert.Equal("int4", Precision.INT4.ToValue());
+ }
+
+ [Fact]
+ public void Priority_HasAllValues()
+ {
+ Assert.Equal("critical", Priority.Critical.ToValue());
+ Assert.Equal("high", Priority.High.ToValue());
+ Assert.Equal("normal", Priority.Normal.ToValue());
+ Assert.Equal("low", Priority.Low.ToValue());
+ Assert.Equal("background", Priority.Background.ToValue());
+ }
+
+ [Fact]
+ public void JobStatus_HasAllValues()
+ {
+ Assert.Equal("pending", JobStatus.Pending.ToValue());
+ Assert.Equal("queued", JobStatus.Queued.ToValue());
+ Assert.Equal("running", JobStatus.Running.ToValue());
+ Assert.Equal("completed", JobStatus.Completed.ToValue());
+ Assert.Equal("failed", JobStatus.Failed.ToValue());
+ Assert.Equal("cancelled", JobStatus.Cancelled.ToValue());
+ }
+
+ [Fact]
+ public void JobStatus_IdentifiesTerminalStates()
+ {
+ Assert.True(JobStatus.Completed.IsTerminal());
+ Assert.True(JobStatus.Failed.IsTerminal());
+ Assert.True(JobStatus.Cancelled.IsTerminal());
+ Assert.False(JobStatus.Pending.IsTerminal());
+ Assert.False(JobStatus.Running.IsTerminal());
+ }
+
+ [Fact]
+ public void SynorConfig_CreatesWithDefaults()
+ {
+ var config = new SynorConfig("test-key");
+
+ Assert.Equal("test-key", config.ApiKey);
+ Assert.Equal("https://api.synor.io/compute/v1", config.BaseUrl);
+ Assert.Equal(TimeSpan.FromSeconds(30), config.Timeout);
+ Assert.Equal(ProcessorType.Auto, config.DefaultProcessor);
+ Assert.Equal(Precision.FP32, config.DefaultPrecision);
+ }
+
+ [Fact]
+ public void SynorConfig_CreatesWithCustomValues()
+ {
+ var config = new SynorConfig("test-key")
+ {
+ BaseUrl = "https://custom.api.com",
+ Timeout = TimeSpan.FromSeconds(60),
+ DefaultProcessor = ProcessorType.Gpu,
+ DefaultPrecision = Precision.FP16
+ };
+
+ Assert.Equal("https://custom.api.com", config.BaseUrl);
+ Assert.Equal(TimeSpan.FromSeconds(60), config.Timeout);
+ Assert.Equal(ProcessorType.Gpu, config.DefaultProcessor);
+ }
+
+ [Fact]
+ public void MatMulOptions_HasCorrectDefaults()
+ {
+ var options = new MatMulOptions();
+
+ Assert.Equal(Precision.FP32, options.Precision);
+ Assert.Equal(ProcessorType.Auto, options.Processor);
+ Assert.Equal(Priority.Normal, options.Priority);
+ }
+
+ [Fact]
+ public void Conv2dOptions_HasCorrectDefaults()
+ {
+ var options = new Conv2dOptions();
+
+ Assert.Equal((1, 1), options.Stride);
+ Assert.Equal((0, 0), options.Padding);
+ }
+
+ [Fact]
+ public void AttentionOptions_HasCorrectDefaults()
+ {
+ var options = new AttentionOptions();
+
+ Assert.Equal(8, options.NumHeads);
+ Assert.True(options.Flash);
+ Assert.Equal(Precision.FP16, options.Precision);
+ }
+
+ [Fact]
+ public void InferenceOptions_HasCorrectDefaults()
+ {
+ var options = new InferenceOptions();
+
+ Assert.Equal(256, options.MaxTokens);
+ Assert.Equal(0.7, options.Temperature, 3);
+ Assert.Equal(0.9, options.TopP, 3);
+ Assert.Equal(50, options.TopK);
+ }
+
+ [Fact]
+ public void JobResult_IdentifiesSuccess()
+ {
+ var result = new JobResult
+ {
+ JobId = "job-123",
+ Status = JobStatus.Completed,
+ Result = "output"
+ };
+
+ Assert.True(result.IsSuccess);
+ Assert.False(result.IsFailed);
+ }
+
+ [Fact]
+ public void JobResult_IdentifiesFailure()
+ {
+ var result = new JobResult
+ {
+ JobId = "job-456",
+ Status = JobStatus.Failed,
+ Error = "Error message"
+ };
+
+ Assert.False(result.IsSuccess);
+ Assert.True(result.IsFailed);
+ }
+
+ [Fact]
+ public void ModelInfo_FormatsParametersCorrectly()
+ {
+ var model = new ModelInfo
+ {
+ Id = "test",
+ Name = "Test Model",
+ Category = "llm",
+ Parameters = 70_000_000_000
+ };
+
+ Assert.Equal("70B", model.FormattedParameters);
+ }
+
+ [Fact]
+ public void ModelInfo_FormatsParametersInMillions()
+ {
+ var model = new ModelInfo
+ {
+ Id = "test",
+ Name = "Test Model",
+ Category = "embedding",
+ Parameters = 350_000_000
+ };
+
+ Assert.Equal("350M", model.FormattedParameters);
+ }
+}
diff --git a/sdk/flutter/test/tensor_test.dart b/sdk/flutter/test/tensor_test.dart
new file mode 100644
index 0000000..ea3aaca
--- /dev/null
+++ b/sdk/flutter/test/tensor_test.dart
@@ -0,0 +1,400 @@
+import 'dart:convert';
+import 'dart:typed_data';
+
+import 'package:flutter_test/flutter_test.dart';
+import 'package:synor_compute/synor_compute.dart';
+
+void main() {
+ group('Tensor Creation', () {
+ test('creates tensor with shape and data', () {
+ final tensor = Tensor(
+ shape: [2, 3],
+ data: [1.0, 2.0, 3.0, 4.0, 5.0, 6.0],
+ );
+
+ expect(tensor.shape, equals([2, 3]));
+ expect(tensor.size, equals(6));
+ expect(tensor.ndim, equals(2));
+ });
+
+ test('throws on data-shape mismatch', () {
+ expect(
+ () => Tensor(
+ shape: [2, 3],
+ data: [1.0, 2.0, 3.0], // Only 3 elements for shape [2, 3]
+ ),
+ throwsA(isA()),
+ );
+ });
+
+ test('creates tensor from typed data', () {
+ final data = Float64List.fromList([1.0, 2.0, 3.0, 4.0]);
+ final tensor = Tensor.fromTypedData(
+ shape: [2, 2],
+ data: data,
+ );
+
+ expect(tensor.shape, equals([2, 2]));
+ expect(tensor.size, equals(4));
+ });
+ });
+
+ group('Tensor Factory Methods', () {
+ test('creates zeros tensor', () {
+ final tensor = Tensor.zeros([3, 4]);
+
+ expect(tensor.shape, equals([3, 4]));
+ expect(tensor.size, equals(12));
+ expect(tensor.data.every((v) => v == 0.0), isTrue);
+ });
+
+ test('creates ones tensor', () {
+ final tensor = Tensor.ones([2, 2]);
+
+ expect(tensor.shape, equals([2, 2]));
+ expect(tensor.data.every((v) => v == 1.0), isTrue);
+ });
+
+ test('creates full tensor with value', () {
+ final tensor = Tensor.full([3, 3], 5.0);
+
+ expect(tensor.data.every((v) => v == 5.0), isTrue);
+ });
+
+ test('creates random tensor', () {
+ final tensor = Tensor.rand([10, 10]);
+
+ expect(tensor.shape, equals([10, 10]));
+ expect(tensor.size, equals(100));
+ // Values should be in [0, 1)
+ expect(tensor.data.every((v) => v >= 0 && v < 1), isTrue);
+ });
+
+ test('creates randn tensor with normal distribution', () {
+ final tensor = Tensor.randn([1000]);
+
+ // Mean should be close to 0, std close to 1
+ expect(tensor.mean().abs(), lessThan(0.2));
+ expect(tensor.std(), closeTo(1.0, 0.2));
+ });
+
+ test('creates identity matrix', () {
+ final tensor = Tensor.eye(3);
+
+ expect(tensor.shape, equals([3, 3]));
+ expect(tensor.at([0, 0]), equals(1.0));
+ expect(tensor.at([1, 1]), equals(1.0));
+ expect(tensor.at([2, 2]), equals(1.0));
+ expect(tensor.at([0, 1]), equals(0.0));
+ });
+
+ test('creates linspace tensor', () {
+ final tensor = Tensor.linspace(0.0, 10.0, 11);
+
+ expect(tensor.shape, equals([11]));
+ expect(tensor[0], equals(0.0));
+ expect(tensor[10], equals(10.0));
+ expect(tensor[5], equals(5.0));
+ });
+
+ test('creates arange tensor', () {
+ final tensor = Tensor.arange(0.0, 5.0, step: 1.0);
+
+ expect(tensor.shape, equals([5]));
+ expect(tensor[0], equals(0.0));
+ expect(tensor[4], equals(4.0));
+ });
+ });
+
+ group('Tensor Operations', () {
+ test('reshapes tensor', () {
+ final tensor = Tensor(
+ shape: [6],
+ data: [1.0, 2.0, 3.0, 4.0, 5.0, 6.0],
+ );
+
+ final reshaped = tensor.reshape([2, 3]);
+
+ expect(reshaped.shape, equals([2, 3]));
+ expect(reshaped.size, equals(6));
+ });
+
+ test('throws on invalid reshape', () {
+ final tensor = Tensor.zeros([4]);
+
+ expect(
+ () => tensor.reshape([2, 3]),
+ throwsA(isA()),
+ );
+ });
+
+ test('flattens tensor', () {
+ final tensor = Tensor.zeros([2, 3, 4]);
+ final flat = tensor.flatten();
+
+ expect(flat.shape, equals([24]));
+ });
+
+ test('transposes 2D tensor', () {
+ final tensor = Tensor(
+ shape: [2, 3],
+ data: [1.0, 2.0, 3.0, 4.0, 5.0, 6.0],
+ );
+
+ final transposed = tensor.transpose();
+
+ expect(transposed.shape, equals([3, 2]));
+ expect(transposed.at([0, 0]), equals(1.0));
+ expect(transposed.at([0, 1]), equals(4.0));
+ });
+
+ test('accesses element at index for 1D tensor', () {
+ final tensor = Tensor(shape: [4], data: [1.0, 2.0, 3.0, 4.0]);
+
+ expect(tensor[0], equals(1.0));
+ expect(tensor[3], equals(4.0));
+ });
+
+ test('accesses element at multi-dimensional index', () {
+ final tensor = Tensor(
+ shape: [2, 3],
+ data: [1.0, 2.0, 3.0, 4.0, 5.0, 6.0],
+ );
+
+ expect(tensor.at([0, 0]), equals(1.0));
+ expect(tensor.at([0, 2]), equals(3.0));
+ expect(tensor.at([1, 1]), equals(5.0));
+ });
+
+ test('throws on out-of-bounds index', () {
+ final tensor = Tensor.zeros([2, 3]);
+
+ expect(
+ () => tensor.at([2, 0]),
+ throwsA(isA()),
+ );
+ });
+ });
+
+ group('Tensor Reductions', () {
+ test('calculates sum', () {
+ final tensor = Tensor(shape: [4], data: [1.0, 2.0, 3.0, 4.0]);
+
+ expect(tensor.sum(), equals(10.0));
+ });
+
+ test('calculates mean', () {
+ final tensor = Tensor(shape: [4], data: [1.0, 2.0, 3.0, 4.0]);
+
+ expect(tensor.mean(), equals(2.5));
+ });
+
+ test('calculates std', () {
+ final tensor = Tensor(shape: [4], data: [1.0, 2.0, 3.0, 4.0]);
+
+ expect(tensor.std(), closeTo(1.118, 0.001));
+ });
+
+ test('finds min', () {
+ final tensor = Tensor(shape: [4], data: [3.0, 1.0, 4.0, 2.0]);
+
+ expect(tensor.min(), equals(1.0));
+ });
+
+ test('finds max', () {
+ final tensor = Tensor(shape: [4], data: [3.0, 1.0, 4.0, 2.0]);
+
+ expect(tensor.max(), equals(4.0));
+ });
+
+ test('finds argmin', () {
+ final tensor = Tensor(shape: [4], data: [3.0, 1.0, 4.0, 2.0]);
+
+ expect(tensor.argmin(), equals(1));
+ });
+
+ test('finds argmax', () {
+ final tensor = Tensor(shape: [4], data: [3.0, 1.0, 4.0, 2.0]);
+
+ expect(tensor.argmax(), equals(2));
+ });
+ });
+
+ group('Tensor Element-wise Operations', () {
+ test('adds tensors', () {
+ final a = Tensor(shape: [3], data: [1.0, 2.0, 3.0]);
+ final b = Tensor(shape: [3], data: [4.0, 5.0, 6.0]);
+
+ final result = a.add(b);
+
+ expect(result.data, equals(Float64List.fromList([5.0, 7.0, 9.0])));
+ });
+
+ test('subtracts tensors', () {
+ final a = Tensor(shape: [3], data: [5.0, 7.0, 9.0]);
+ final b = Tensor(shape: [3], data: [1.0, 2.0, 3.0]);
+
+ final result = a.sub(b);
+
+ expect(result.data, equals(Float64List.fromList([4.0, 5.0, 6.0])));
+ });
+
+ test('multiplies tensors', () {
+ final a = Tensor(shape: [3], data: [2.0, 3.0, 4.0]);
+ final b = Tensor(shape: [3], data: [1.0, 2.0, 3.0]);
+
+ final result = a.mul(b);
+
+ expect(result.data, equals(Float64List.fromList([2.0, 6.0, 12.0])));
+ });
+
+ test('divides tensors', () {
+ final a = Tensor(shape: [3], data: [6.0, 8.0, 9.0]);
+ final b = Tensor(shape: [3], data: [2.0, 4.0, 3.0]);
+
+ final result = a.div(b);
+
+ expect(result.data, equals(Float64List.fromList([3.0, 2.0, 3.0])));
+ });
+
+ test('adds scalar', () {
+ final tensor = Tensor(shape: [3], data: [1.0, 2.0, 3.0]);
+ final result = tensor.addScalar(10.0);
+
+ expect(result.data, equals(Float64List.fromList([11.0, 12.0, 13.0])));
+ });
+
+ test('multiplies by scalar', () {
+ final tensor = Tensor(shape: [3], data: [1.0, 2.0, 3.0]);
+ final result = tensor.mulScalar(2.0);
+
+ expect(result.data, equals(Float64List.fromList([2.0, 4.0, 6.0])));
+ });
+
+ test('throws on shape mismatch', () {
+ final a = Tensor.zeros([2, 3]);
+ final b = Tensor.zeros([3, 2]);
+
+ expect(() => a.add(b), throwsA(isA()));
+ });
+ });
+
+ group('Tensor Activations', () {
+ test('applies relu', () {
+ final tensor = Tensor(shape: [5], data: [-2.0, -1.0, 0.0, 1.0, 2.0]);
+ final result = tensor.relu();
+
+ expect(result.data, equals(Float64List.fromList([0.0, 0.0, 0.0, 1.0, 2.0])));
+ });
+
+ test('applies sigmoid', () {
+ final tensor = Tensor(shape: [1], data: [0.0]);
+ final result = tensor.sigmoid();
+
+ expect(result[0], closeTo(0.5, 0.001));
+ });
+
+ test('applies tanh', () {
+ final tensor = Tensor(shape: [1], data: [0.0]);
+ final result = tensor.tanh();
+
+ expect(result[0], closeTo(0.0, 0.001));
+ });
+
+ test('applies softmax on 1D tensor', () {
+ final tensor = Tensor(shape: [3], data: [1.0, 2.0, 3.0]);
+ final result = tensor.softmax();
+
+ expect(result.sum(), closeTo(1.0, 0.001));
+ expect(result[2], greaterThan(result[1]));
+ expect(result[1], greaterThan(result[0]));
+ });
+ });
+
+ group('Tensor Serialization', () {
+ test('serializes to JSON', () {
+ final tensor = Tensor(
+ shape: [2, 2],
+ data: [1.0, 2.0, 3.0, 4.0],
+ );
+
+ final json = tensor.toJson();
+
+ expect(json['shape'], equals([2, 2]));
+ expect(json['dtype'], equals('float64'));
+ expect(json['data'], isA());
+ });
+
+ test('deserializes from JSON with base64 data', () {
+ final original = Tensor(
+ shape: [2, 3],
+ data: [1.0, 2.0, 3.0, 4.0, 5.0, 6.0],
+ );
+ final json = original.toJson();
+ final restored = Tensor.fromJson(json);
+
+ expect(restored.shape, equals(original.shape));
+ expect(restored.dtype, equals(original.dtype));
+ for (var i = 0; i < original.size; i++) {
+ expect(restored.data[i], closeTo(original.data[i], 0.0001));
+ }
+ });
+
+ test('deserializes from JSON with list data', () {
+ final json = {
+ 'shape': [2, 2],
+ 'data': [
+ [1.0, 2.0],
+ [3.0, 4.0]
+ ],
+ 'dtype': 'float64',
+ };
+
+ final tensor = Tensor.fromJson(json);
+
+ expect(tensor.shape, equals([2, 2]));
+ expect(tensor.at([0, 0]), equals(1.0));
+ expect(tensor.at([1, 1]), equals(4.0));
+ });
+
+ test('converts to nested list', () {
+ final tensor = Tensor(
+ shape: [2, 3],
+ data: [1.0, 2.0, 3.0, 4.0, 5.0, 6.0],
+ );
+
+ final nested = tensor.toNestedList();
+
+ expect(nested, equals([
+ [1.0, 2.0, 3.0],
+ [4.0, 5.0, 6.0],
+ ]));
+ });
+ });
+
+ group('Tensor Properties', () {
+ test('calculates nbytes correctly', () {
+ final tensor = Tensor.zeros([10]);
+
+ // Float64List uses 8 bytes per element
+ expect(tensor.nbytes, equals(80));
+ });
+
+ test('equality works correctly', () {
+ final a = Tensor(shape: [2], data: [1.0, 2.0]);
+ final b = Tensor(shape: [2], data: [1.0, 2.0]);
+ final c = Tensor(shape: [2], data: [1.0, 3.0]);
+
+ expect(a, equals(b));
+ expect(a, isNot(equals(c)));
+ });
+
+ test('toString provides useful info', () {
+ final small = Tensor(shape: [2], data: [1.0, 2.0]);
+ final large = Tensor.zeros([100, 100]);
+
+ expect(small.toString(), contains('Tensor'));
+ expect(large.toString(), contains('shape'));
+ });
+ });
+}
diff --git a/sdk/flutter/test/types_test.dart b/sdk/flutter/test/types_test.dart
new file mode 100644
index 0000000..cbc3dcd
--- /dev/null
+++ b/sdk/flutter/test/types_test.dart
@@ -0,0 +1,462 @@
+import 'package:flutter_test/flutter_test.dart';
+import 'package:synor_compute/synor_compute.dart';
+
+void main() {
+ group('Precision Enum', () {
+ test('has all expected values', () {
+ expect(Precision.values.length, equals(6));
+ expect(Precision.fp64.value, equals('fp64'));
+ expect(Precision.fp32.value, equals('fp32'));
+ expect(Precision.fp16.value, equals('fp16'));
+ expect(Precision.bf16.value, equals('bf16'));
+ expect(Precision.int8.value, equals('int8'));
+ expect(Precision.int4.value, equals('int4'));
+ });
+
+ test('parses from string', () {
+ expect(Precision.fromString('fp16'), equals(Precision.fp16));
+ expect(Precision.fromString('bf16'), equals(Precision.bf16));
+ expect(Precision.fromString('invalid'), equals(Precision.fp32)); // default
+ });
+ });
+
+ group('ProcessorType Enum', () {
+ test('has all expected values', () {
+ expect(ProcessorType.values.length, equals(10));
+ expect(ProcessorType.cpu.value, equals('cpu'));
+ expect(ProcessorType.gpu.value, equals('gpu'));
+ expect(ProcessorType.tpu.value, equals('tpu'));
+ expect(ProcessorType.npu.value, equals('npu'));
+ expect(ProcessorType.lpu.value, equals('lpu'));
+ expect(ProcessorType.auto.value, equals('auto'));
+ });
+
+ test('parses from string', () {
+ expect(ProcessorType.fromString('gpu'), equals(ProcessorType.gpu));
+ expect(ProcessorType.fromString('invalid'), equals(ProcessorType.auto));
+ });
+ });
+
+ group('Priority Enum', () {
+ test('has all expected values', () {
+ expect(Priority.values.length, equals(4));
+ expect(Priority.low.value, equals('low'));
+ expect(Priority.normal.value, equals('normal'));
+ expect(Priority.high.value, equals('high'));
+ expect(Priority.critical.value, equals('critical'));
+ });
+
+ test('parses from string', () {
+ expect(Priority.fromString('high'), equals(Priority.high));
+ expect(Priority.fromString('invalid'), equals(Priority.normal));
+ });
+ });
+
+ group('JobStatus Enum', () {
+ test('has all expected values', () {
+ expect(JobStatus.values.length, equals(6));
+ expect(JobStatus.pending.value, equals('pending'));
+ expect(JobStatus.queued.value, equals('queued'));
+ expect(JobStatus.running.value, equals('running'));
+ expect(JobStatus.completed.value, equals('completed'));
+ expect(JobStatus.failed.value, equals('failed'));
+ expect(JobStatus.cancelled.value, equals('cancelled'));
+ });
+
+ test('identifies terminal states', () {
+ expect(JobStatus.completed.isTerminal, isTrue);
+ expect(JobStatus.failed.isTerminal, isTrue);
+ expect(JobStatus.cancelled.isTerminal, isTrue);
+ expect(JobStatus.pending.isTerminal, isFalse);
+ expect(JobStatus.running.isTerminal, isFalse);
+ });
+ });
+
+ group('BalancingStrategy Enum', () {
+ test('has all expected values', () {
+ expect(BalancingStrategy.speed.value, equals('speed'));
+ expect(BalancingStrategy.cost.value, equals('cost'));
+ expect(BalancingStrategy.energy.value, equals('energy'));
+ expect(BalancingStrategy.balanced.value, equals('balanced'));
+ });
+ });
+
+ group('DType Enum', () {
+ test('has all expected values', () {
+ expect(DType.float64.value, equals('float64'));
+ expect(DType.float32.value, equals('float32'));
+ expect(DType.int8.value, equals('int8'));
+ expect(DType.bool_.value, equals('bool'));
+ });
+ });
+
+ group('ModelCategory Enum', () {
+ test('has all expected values', () {
+ expect(ModelCategory.llm.value, equals('llm'));
+ expect(ModelCategory.embedding.value, equals('embedding'));
+ expect(ModelCategory.imageGeneration.value, equals('image_generation'));
+ expect(ModelCategory.custom.value, equals('custom'));
+ });
+ });
+
+ group('SynorConfig', () {
+ test('creates with required fields', () {
+ final config = SynorConfig(apiKey: 'test-key');
+
+ expect(config.apiKey, equals('test-key'));
+ expect(config.baseUrl, equals('https://compute.synor.io'));
+ expect(config.timeout, equals(const Duration(seconds: 30)));
+ expect(config.maxRetries, equals(3));
+ expect(config.defaultProcessor, equals(ProcessorType.auto));
+ expect(config.defaultPrecision, equals(Precision.fp32));
+ });
+
+ test('creates with custom values', () {
+ final config = SynorConfig(
+ apiKey: 'test-key',
+ baseUrl: 'https://custom.api.com',
+ timeout: const Duration(seconds: 60),
+ maxRetries: 5,
+ defaultProcessor: ProcessorType.gpu,
+ defaultPrecision: Precision.fp16,
+ );
+
+ expect(config.baseUrl, equals('https://custom.api.com'));
+ expect(config.timeout, equals(const Duration(seconds: 60)));
+ expect(config.maxRetries, equals(5));
+ });
+
+ test('copyWith updates fields', () {
+ final original = SynorConfig(apiKey: 'test-key');
+ final updated = original.copyWith(
+ timeout: const Duration(seconds: 60),
+ defaultProcessor: ProcessorType.tpu,
+ );
+
+ expect(updated.apiKey, equals('test-key'));
+ expect(updated.timeout, equals(const Duration(seconds: 60)));
+ expect(updated.defaultProcessor, equals(ProcessorType.tpu));
+ });
+ });
+
+ group('MatMulOptions', () {
+ test('creates with defaults', () {
+ const options = MatMulOptions();
+
+ expect(options.precision, isNull);
+ expect(options.processor, isNull);
+ expect(options.transposeA, isFalse);
+ expect(options.transposeB, isFalse);
+ });
+
+ test('toJson generates correct output', () {
+ const options = MatMulOptions(
+ precision: Precision.fp16,
+ processor: ProcessorType.gpu,
+ transposeA: true,
+ );
+
+ final json = options.toJson();
+
+ expect(json['precision'], equals('fp16'));
+ expect(json['processor'], equals('gpu'));
+ expect(json['transpose_a'], isTrue);
+ expect(json['transpose_b'], isFalse);
+ });
+ });
+
+ group('Conv2dOptions', () {
+ test('creates with defaults', () {
+ const options = Conv2dOptions();
+
+ expect(options.kernel, equals([3, 3]));
+ expect(options.stride, equals([1, 1]));
+ expect(options.padding, equals([0, 0]));
+ expect(options.groups, equals(1));
+ });
+
+ test('toJson generates correct output', () {
+ const options = Conv2dOptions(
+ kernel: [5, 5],
+ stride: [2, 2],
+ padding: [2, 2],
+ );
+
+ final json = options.toJson();
+
+ expect(json['kernel'], equals([5, 5]));
+ expect(json['stride'], equals([2, 2]));
+ expect(json['padding'], equals([2, 2]));
+ });
+ });
+
+ group('AttentionOptions', () {
+ test('creates with required fields', () {
+ const options = AttentionOptions(numHeads: 8);
+
+ expect(options.numHeads, equals(8));
+ expect(options.causal, isFalse);
+ expect(options.scale, isNull);
+ });
+
+ test('toJson generates correct output', () {
+ const options = AttentionOptions(
+ numHeads: 12,
+ scale: 0.125,
+ causal: true,
+ precision: Precision.fp16,
+ );
+
+ final json = options.toJson();
+
+ expect(json['num_heads'], equals(12));
+ expect(json['scale'], equals(0.125));
+ expect(json['causal'], isTrue);
+ expect(json['precision'], equals('fp16'));
+ });
+ });
+
+ group('InferenceOptions', () {
+ test('creates with defaults', () {
+ const options = InferenceOptions();
+
+ expect(options.maxTokens, equals(256));
+ expect(options.temperature, equals(0.7));
+ expect(options.topP, equals(1.0));
+ expect(options.stream, isFalse);
+ });
+
+ test('toJson generates correct output', () {
+ const options = InferenceOptions(
+ maxTokens: 512,
+ temperature: 0.9,
+ topK: 50,
+ stream: true,
+ );
+
+ final json = options.toJson();
+
+ expect(json['max_tokens'], equals(512));
+ expect(json['temperature'], equals(0.9));
+ expect(json['top_k'], equals(50));
+ expect(json['stream'], isTrue);
+ });
+ });
+
+ group('PricingInfo', () {
+ test('parses from JSON', () {
+ final json = {
+ 'processor': 'gpu',
+ 'price_per_second': 0.0001,
+ 'price_per_gflop': 0.000001,
+ 'available_units': 100,
+ 'utilization_percent': 75.5,
+ 'region': 'us-east-1',
+ };
+
+ final pricing = PricingInfo.fromJson(json);
+
+ expect(pricing.processor, equals(ProcessorType.gpu));
+ expect(pricing.pricePerSecond, equals(0.0001));
+ expect(pricing.availableUnits, equals(100));
+ expect(pricing.utilizationPercent, equals(75.5));
+ });
+ });
+
+ group('UsageStats', () {
+ test('parses from JSON', () {
+ final json = {
+ 'total_jobs': 1000,
+ 'completed_jobs': 950,
+ 'failed_jobs': 50,
+ 'total_compute_seconds': 3600.0,
+ 'total_cost': 0.36,
+ 'cost_by_processor': {
+ 'gpu': 0.30,
+ 'tpu': 0.06,
+ },
+ };
+
+ final stats = UsageStats.fromJson(json);
+
+ expect(stats.totalJobs, equals(1000));
+ expect(stats.completedJobs, equals(950));
+ expect(stats.failedJobs, equals(50));
+ expect(stats.totalComputeSeconds, equals(3600.0));
+ expect(stats.costByProcessor[ProcessorType.gpu], equals(0.30));
+ });
+ });
+
+ group('ModelInfo', () {
+ test('parses from JSON', () {
+ final json = {
+ 'id': 'llama-3-70b',
+ 'name': 'Llama 3 70B',
+ 'description': 'Large language model',
+ 'category': 'llm',
+ 'cid': 'QmTest...',
+ 'format': 'safetensors',
+ 'size_bytes': 140000000000,
+ 'parameters': 70000000000,
+ 'supported_precisions': ['fp16', 'bf16', 'fp32'],
+ 'recommended_processor': 'gpu',
+ 'context_length': 8192,
+ 'license': 'llama3',
+ 'provider': 'meta',
+ 'version': '1.0.0',
+ 'is_public': true,
+ };
+
+ final model = ModelInfo.fromJson(json);
+
+ expect(model.id, equals('llama-3-70b'));
+ expect(model.category, equals(ModelCategory.llm));
+ expect(model.parameters, equals(70000000000));
+ expect(model.contextLength, equals(8192));
+ });
+
+ test('formats parameters correctly', () {
+ final model = ModelInfo(
+ id: 'test',
+ name: 'Test',
+ description: 'Test',
+ category: ModelCategory.llm,
+ cid: 'QmTest',
+ format: ModelFormat.onnx,
+ sizeBytes: 1000,
+ parameters: 70000000000,
+ supportedPrecisions: [Precision.fp16],
+ recommendedProcessor: ProcessorType.gpu,
+ license: 'MIT',
+ provider: 'test',
+ version: '1.0',
+ isPublic: true,
+ );
+
+ expect(model.formattedParameters, equals('70.0B'));
+ });
+ });
+
+ group('TrainingOptions', () {
+ test('creates with defaults', () {
+ const options = TrainingOptions();
+
+ expect(options.framework, equals(MlFramework.pytorch));
+ expect(options.epochs, equals(1));
+ expect(options.batchSize, equals(32));
+ expect(options.learningRate, equals(0.001));
+ });
+
+ test('toJson generates correct output', () {
+ const options = TrainingOptions(
+ framework: MlFramework.jax,
+ epochs: 10,
+ batchSize: 64,
+ learningRate: 0.0001,
+ distributed: true,
+ );
+
+ final json = options.toJson();
+
+ expect(json['framework'], equals('jax'));
+ expect(json['epochs'], equals(10));
+ expect(json['batch_size'], equals(64));
+ expect(json['distributed'], isTrue);
+ });
+ });
+
+ group('TrainingResult', () {
+ test('parses from JSON', () {
+ final json = {
+ 'job_id': 'train-123',
+ 'model_cid': 'QmTrained...',
+ 'epochs': 10,
+ 'final_loss': 0.05,
+ 'metrics': {'accuracy': 0.95, 'f1': 0.94},
+ 'duration_ms': 3600000,
+ 'cost': 1.50,
+ 'checkpoint_cids': ['QmChk1', 'QmChk2'],
+ };
+
+ final result = TrainingResult.fromJson(json);
+
+ expect(result.jobId, equals('train-123'));
+ expect(result.epochs, equals(10));
+ expect(result.finalLoss, equals(0.05));
+ expect(result.metrics['accuracy'], equals(0.95));
+ });
+ });
+
+ group('DatasetUploadOptions', () {
+ test('creates with defaults', () {
+ const options = DatasetUploadOptions(name: 'test-dataset');
+
+ expect(options.name, equals('test-dataset'));
+ expect(options.format, equals(DatasetFormat.jsonl));
+ expect(options.type, equals(DatasetType.textCompletion));
+ expect(options.isPublic, isFalse);
+ });
+
+ test('toJson generates correct output', () {
+ const options = DatasetUploadOptions(
+ name: 'my-dataset',
+ description: 'Test dataset',
+ format: DatasetFormat.parquet,
+ type: DatasetType.instructionTuning,
+ isPublic: true,
+ );
+
+ final json = options.toJson();
+
+ expect(json['name'], equals('my-dataset'));
+ expect(json['format'], equals('parquet'));
+ expect(json['type'], equals('instruction_tuning'));
+ expect(json['is_public'], isTrue);
+ });
+ });
+
+ group('TrainingProgress', () {
+ test('parses from JSON', () {
+ final json = {
+ 'job_id': 'train-123',
+ 'epoch': 5,
+ 'total_epochs': 10,
+ 'step': 500,
+ 'total_steps': 1000,
+ 'loss': 0.15,
+ 'metrics': {'accuracy': 0.85},
+ 'learning_rate': 0.0001,
+ 'samples_per_second': 100,
+ };
+
+ final progress = TrainingProgress.fromJson(json);
+
+ expect(progress.epoch, equals(5));
+ expect(progress.progress, equals(0.5));
+ expect(progress.progressText, contains('Epoch 6/10'));
+ });
+ });
+
+ group('SynorException', () {
+ test('creates with message', () {
+ const exception = SynorException('Test error', code: 'ERR_TEST');
+
+ expect(exception.message, equals('Test error'));
+ expect(exception.code, equals('ERR_TEST'));
+ expect(exception.toString(), contains('Test error'));
+ });
+
+ test('parses from JSON', () {
+ final json = {
+ 'message': 'API Error',
+ 'code': 'API_ERR',
+ 'status_code': 500,
+ };
+
+ final exception = SynorException.fromJson(json);
+
+ expect(exception.message, equals('API Error'));
+ expect(exception.statusCode, equals(500));
+ });
+ });
+}
diff --git a/sdk/go/synor_test.go b/sdk/go/synor_test.go
new file mode 100644
index 0000000..142abdc
--- /dev/null
+++ b/sdk/go/synor_test.go
@@ -0,0 +1,416 @@
+package synor
+
+import (
+ "context"
+ "encoding/json"
+ "net/http"
+ "net/http/httptest"
+ "testing"
+ "time"
+)
+
+// TestClientCreation tests client initialization.
+func TestClientCreation(t *testing.T) {
+ t.Run("creates client with API key", func(t *testing.T) {
+ client := NewClient("test-api-key")
+ if client == nil {
+ t.Fatal("expected non-nil client")
+ }
+ })
+
+ t.Run("creates client with config", func(t *testing.T) {
+ config := Config{
+ APIKey: "test-api-key",
+ Endpoint: "https://custom.api.com",
+ Strategy: Cost,
+ Precision: FP16,
+ Timeout: 60 * time.Second,
+ }
+ client := NewClientWithConfig(config)
+ if client == nil {
+ t.Fatal("expected non-nil client")
+ }
+ if client.config.Endpoint != "https://custom.api.com" {
+ t.Errorf("expected custom endpoint, got %s", client.config.Endpoint)
+ }
+ })
+}
+
+// TestDefaultConfig tests default configuration.
+func TestDefaultConfig(t *testing.T) {
+ config := DefaultConfig("my-key")
+
+ if config.APIKey != "my-key" {
+ t.Errorf("expected API key 'my-key', got %s", config.APIKey)
+ }
+ if config.Endpoint != DefaultEndpoint {
+ t.Errorf("expected default endpoint, got %s", config.Endpoint)
+ }
+ if config.Strategy != Balanced {
+ t.Errorf("expected Balanced strategy, got %s", config.Strategy)
+ }
+ if config.Precision != FP32 {
+ t.Errorf("expected FP32 precision, got %s", config.Precision)
+ }
+ if config.Timeout != 30*time.Second {
+ t.Errorf("expected 30s timeout, got %v", config.Timeout)
+ }
+}
+
+// TestTensorCreation tests tensor creation.
+func TestTensorCreation(t *testing.T) {
+ t.Run("creates tensor from data", func(t *testing.T) {
+ data := []float32{1.0, 2.0, 3.0, 4.0, 5.0, 6.0}
+ shape := []int{2, 3}
+ tensor := NewTensor(data, shape, FP32)
+
+ if tensor == nil {
+ t.Fatal("expected non-nil tensor")
+ }
+ if len(tensor.Data) != 6 {
+ t.Errorf("expected 6 elements, got %d", len(tensor.Data))
+ }
+ if len(tensor.Shape) != 2 {
+ t.Errorf("expected 2 dimensions, got %d", len(tensor.Shape))
+ }
+ if tensor.DType != FP32 {
+ t.Errorf("expected FP32 dtype, got %s", tensor.DType)
+ }
+ })
+
+ t.Run("creates zeros tensor", func(t *testing.T) {
+ tensor := Zeros([]int{3, 4}, FP32)
+
+ if tensor == nil {
+ t.Fatal("expected non-nil tensor")
+ }
+ if len(tensor.Data) != 12 {
+ t.Errorf("expected 12 elements, got %d", len(tensor.Data))
+ }
+ for i, v := range tensor.Data {
+ if v != 0.0 {
+ t.Errorf("expected 0 at index %d, got %f", i, v)
+ }
+ }
+ })
+}
+
+// TestTensorSerialize tests tensor serialization.
+func TestTensorSerialize(t *testing.T) {
+ data := []float32{1.0, 2.0, 3.0, 4.0}
+ shape := []int{2, 2}
+ tensor := NewTensor(data, shape, FP32)
+
+ serialized := tensor.Serialize()
+
+ if _, ok := serialized["data"]; !ok {
+ t.Error("expected 'data' field in serialized tensor")
+ }
+ if _, ok := serialized["shape"]; !ok {
+ t.Error("expected 'shape' field in serialized tensor")
+ }
+ if _, ok := serialized["dtype"]; !ok {
+ t.Error("expected 'dtype' field in serialized tensor")
+ }
+ if serialized["dtype"] != FP32 {
+ t.Errorf("expected FP32 dtype, got %v", serialized["dtype"])
+ }
+}
+
+// TestProcessorTypes tests processor type constants.
+func TestProcessorTypes(t *testing.T) {
+ types := []ProcessorType{CPU, GPU, TPU, NPU, LPU, FPGA, WASM, WebGPU}
+ expected := []string{"cpu", "gpu", "tpu", "npu", "lpu", "fpga", "wasm", "webgpu"}
+
+ for i, pt := range types {
+ if string(pt) != expected[i] {
+ t.Errorf("expected %s, got %s", expected[i], pt)
+ }
+ }
+}
+
+// TestPrecisionTypes tests precision type constants.
+func TestPrecisionTypes(t *testing.T) {
+ precisions := []Precision{FP64, FP32, FP16, BF16, INT8, INT4}
+ expected := []string{"fp64", "fp32", "fp16", "bf16", "int8", "int4"}
+
+ for i, p := range precisions {
+ if string(p) != expected[i] {
+ t.Errorf("expected %s, got %s", expected[i], p)
+ }
+ }
+}
+
+// TestJobStatus tests job status constants.
+func TestJobStatus(t *testing.T) {
+ statuses := []JobStatus{Pending, Queued, Running, Completed, Failed, Cancelled}
+ expected := []string{"pending", "queued", "running", "completed", "failed", "cancelled"}
+
+ for i, s := range statuses {
+ if string(s) != expected[i] {
+ t.Errorf("expected %s, got %s", expected[i], s)
+ }
+ }
+}
+
+// TestBalancingStrategies tests balancing strategy constants.
+func TestBalancingStrategies(t *testing.T) {
+ strategies := []BalancingStrategy{Speed, Cost, Energy, Latency, Balanced}
+ expected := []string{"speed", "cost", "energy", "latency", "balanced"}
+
+ for i, s := range strategies {
+ if string(s) != expected[i] {
+ t.Errorf("expected %s, got %s", expected[i], s)
+ }
+ }
+}
+
+// TestSubmitJob tests job submission with mock server.
+func TestSubmitJob(t *testing.T) {
+ server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ if r.URL.Path == "/jobs" && r.Method == "POST" {
+ w.WriteHeader(http.StatusOK)
+ json.NewEncoder(w).Encode(map[string]interface{}{
+ "job_id": "job-123",
+ })
+ return
+ }
+ w.WriteHeader(http.StatusNotFound)
+ }))
+ defer server.Close()
+
+ config := Config{
+ APIKey: "test-key",
+ Endpoint: server.URL,
+ Timeout: 5 * time.Second,
+ }
+ client := NewClientWithConfig(config)
+
+ ctx := context.Background()
+ job, err := client.SubmitJob(ctx, "matmul", map[string]interface{}{
+ "a": map[string]interface{}{"data": "base64", "shape": []int{2, 2}},
+ "b": map[string]interface{}{"data": "base64", "shape": []int{2, 2}},
+ })
+
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ if job.ID != "job-123" {
+ t.Errorf("expected job ID 'job-123', got %s", job.ID)
+ }
+}
+
+// TestGetJobStatus tests getting job status with mock server.
+func TestGetJobStatus(t *testing.T) {
+ server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ if r.URL.Path == "/jobs/job-123" && r.Method == "GET" {
+ w.WriteHeader(http.StatusOK)
+ json.NewEncoder(w).Encode(map[string]interface{}{
+ "job_id": "job-123",
+ "status": "completed",
+ "data": map[string]interface{}{"result": "success"},
+ })
+ return
+ }
+ w.WriteHeader(http.StatusNotFound)
+ }))
+ defer server.Close()
+
+ config := Config{
+ APIKey: "test-key",
+ Endpoint: server.URL,
+ Timeout: 5 * time.Second,
+ }
+ client := NewClientWithConfig(config)
+
+ ctx := context.Background()
+ result, err := client.GetJobStatus(ctx, "job-123")
+
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ if result.Status != Completed {
+ t.Errorf("expected Completed status, got %s", result.Status)
+ }
+}
+
+// TestGetPricing tests getting pricing information with mock server.
+func TestGetPricing(t *testing.T) {
+ server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ if r.URL.Path == "/pricing" && r.Method == "GET" {
+ w.WriteHeader(http.StatusOK)
+ json.NewEncoder(w).Encode(map[string]interface{}{
+ "pricing": []map[string]interface{}{
+ {
+ "processor_type": "gpu",
+ "spot_price": 0.0001,
+ "avg_price_24h": 0.00012,
+ "aws_equivalent": 0.001,
+ "savings_percent": 90.0,
+ },
+ {
+ "processor_type": "tpu",
+ "spot_price": 0.0002,
+ "avg_price_24h": 0.00022,
+ "aws_equivalent": 0.002,
+ "savings_percent": 89.0,
+ },
+ },
+ })
+ return
+ }
+ w.WriteHeader(http.StatusNotFound)
+ }))
+ defer server.Close()
+
+ config := Config{
+ APIKey: "test-key",
+ Endpoint: server.URL,
+ Timeout: 5 * time.Second,
+ }
+ client := NewClientWithConfig(config)
+
+ ctx := context.Background()
+ pricing, err := client.GetPricing(ctx)
+
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ if len(pricing) != 2 {
+ t.Errorf("expected 2 pricing entries, got %d", len(pricing))
+ }
+}
+
+// TestSynorError tests error formatting.
+func TestSynorError(t *testing.T) {
+ err := &SynorError{
+ Message: "Invalid API key",
+ StatusCode: 401,
+ }
+
+ expected := "synor: Invalid API key (status 401)"
+ if err.Error() != expected {
+ t.Errorf("expected error message '%s', got '%s'", expected, err.Error())
+ }
+}
+
+// TestAPIError tests handling of API errors.
+func TestAPIError(t *testing.T) {
+ server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ w.WriteHeader(http.StatusUnauthorized)
+ json.NewEncoder(w).Encode(map[string]interface{}{
+ "message": "Invalid API key",
+ })
+ }))
+ defer server.Close()
+
+ config := Config{
+ APIKey: "bad-key",
+ Endpoint: server.URL,
+ Timeout: 5 * time.Second,
+ }
+ client := NewClientWithConfig(config)
+
+ ctx := context.Background()
+ _, err := client.SubmitJob(ctx, "test", nil)
+
+ if err == nil {
+ t.Fatal("expected error, got nil")
+ }
+
+ synorErr, ok := err.(*SynorError)
+ if !ok {
+ t.Fatalf("expected SynorError, got %T", err)
+ }
+ if synorErr.StatusCode != 401 {
+ t.Errorf("expected status 401, got %d", synorErr.StatusCode)
+ }
+}
+
+// TestCancelJob tests job cancellation with mock server.
+func TestCancelJob(t *testing.T) {
+ server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ if r.URL.Path == "/jobs/job-123" && r.Method == "DELETE" {
+ w.WriteHeader(http.StatusOK)
+ return
+ }
+ w.WriteHeader(http.StatusNotFound)
+ }))
+ defer server.Close()
+
+ config := Config{
+ APIKey: "test-key",
+ Endpoint: server.URL,
+ Timeout: 5 * time.Second,
+ }
+ client := NewClientWithConfig(config)
+
+ ctx := context.Background()
+ err := client.CancelJob(ctx, "job-123")
+
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+}
+
+// TestJobMetrics tests job metrics structure.
+func TestJobMetrics(t *testing.T) {
+ metrics := JobMetrics{
+ ExecutionTimeMs: 150.5,
+ QueueTimeMs: 10.2,
+ ProcessorType: GPU,
+ ProcessorID: "gpu-001",
+ FLOPS: 1e12,
+ MemoryBytes: 1073741824,
+ CostMicro: 100,
+ EnergyMJ: 5.5,
+ }
+
+ if metrics.ExecutionTimeMs != 150.5 {
+ t.Errorf("expected execution time 150.5, got %f", metrics.ExecutionTimeMs)
+ }
+ if metrics.ProcessorType != GPU {
+ t.Errorf("expected GPU processor, got %s", metrics.ProcessorType)
+ }
+}
+
+// TestPricingInfo tests pricing info structure.
+func TestPricingInfo(t *testing.T) {
+ pricing := PricingInfo{
+ ProcessorType: GPU,
+ SpotPrice: 0.0001,
+ AvgPrice24h: 0.00012,
+ AWSEquivalent: 0.001,
+ SavingsPercent: 90.0,
+ }
+
+ if pricing.ProcessorType != GPU {
+ t.Errorf("expected GPU processor, got %s", pricing.ProcessorType)
+ }
+ if pricing.SavingsPercent != 90.0 {
+ t.Errorf("expected 90%% savings, got %f%%", pricing.SavingsPercent)
+ }
+}
+
+// TestJobResult tests job result structure.
+func TestJobResult(t *testing.T) {
+ result := JobResult{
+ JobID: "job-123",
+ Status: Completed,
+ Data: map[string]interface{}{"output": "success"},
+ Metrics: &JobMetrics{
+ ExecutionTimeMs: 100,
+ ProcessorType: GPU,
+ },
+ }
+
+ if result.JobID != "job-123" {
+ t.Errorf("expected job ID 'job-123', got %s", result.JobID)
+ }
+ if result.Status != Completed {
+ t.Errorf("expected Completed status, got %s", result.Status)
+ }
+ if result.Metrics == nil {
+ t.Error("expected non-nil metrics")
+ }
+}
diff --git a/sdk/java/src/test/java/io/synor/compute/TensorTest.java b/sdk/java/src/test/java/io/synor/compute/TensorTest.java
new file mode 100644
index 0000000..0a9abc8
--- /dev/null
+++ b/sdk/java/src/test/java/io/synor/compute/TensorTest.java
@@ -0,0 +1,239 @@
+package io.synor.compute;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.DisplayName;
+import static org.junit.jupiter.api.Assertions.*;
+
+import java.util.Arrays;
+
+@DisplayName("Tensor Tests")
+class TensorTest {
+
+ @Test
+ @DisplayName("Creates tensor with shape and data")
+ void testCreateTensor() {
+ double[] data = {1.0, 2.0, 3.0, 4.0, 5.0, 6.0};
+ int[] shape = {2, 3};
+ Tensor tensor = new Tensor(shape, data);
+
+ assertArrayEquals(shape, tensor.getShape());
+ assertEquals(6, tensor.getSize());
+ assertEquals(2, tensor.getNdim());
+ }
+
+ @Test
+ @DisplayName("Creates zeros tensor")
+ void testZeros() {
+ Tensor tensor = Tensor.zeros(new int[]{3, 4});
+
+ assertArrayEquals(new int[]{3, 4}, tensor.getShape());
+ assertEquals(12, tensor.getSize());
+ for (double v : tensor.getData()) {
+ assertEquals(0.0, v);
+ }
+ }
+
+ @Test
+ @DisplayName("Creates ones tensor")
+ void testOnes() {
+ Tensor tensor = Tensor.ones(new int[]{2, 2});
+
+ assertArrayEquals(new int[]{2, 2}, tensor.getShape());
+ for (double v : tensor.getData()) {
+ assertEquals(1.0, v);
+ }
+ }
+
+ @Test
+ @DisplayName("Creates random tensor")
+ void testRandom() {
+ Tensor tensor = Tensor.random(new int[]{10, 10});
+
+ assertArrayEquals(new int[]{10, 10}, tensor.getShape());
+ assertEquals(100, tensor.getSize());
+ for (double v : tensor.getData()) {
+ assertTrue(v >= 0 && v < 1, "Random values should be in [0, 1)");
+ }
+ }
+
+ @Test
+ @DisplayName("Creates randn tensor with normal distribution")
+ void testRandn() {
+ Tensor tensor = Tensor.randn(new int[]{1000});
+
+ double mean = tensor.mean();
+ double std = tensor.std();
+
+ assertTrue(Math.abs(mean) < 0.2, "Mean should be close to 0");
+ assertTrue(std > 0.8 && std < 1.2, "Std should be close to 1");
+ }
+
+ @Test
+ @DisplayName("Creates identity matrix")
+ void testEye() {
+ Tensor tensor = Tensor.eye(3);
+
+ assertArrayEquals(new int[]{3, 3}, tensor.getShape());
+ assertEquals(1.0, tensor.get(0, 0));
+ assertEquals(1.0, tensor.get(1, 1));
+ assertEquals(1.0, tensor.get(2, 2));
+ assertEquals(0.0, tensor.get(0, 1));
+ assertEquals(0.0, tensor.get(1, 0));
+ }
+
+ @Test
+ @DisplayName("Creates arange tensor")
+ void testArange() {
+ Tensor tensor = Tensor.arange(0, 5, 1);
+
+ assertArrayEquals(new int[]{5}, tensor.getShape());
+ assertEquals(0.0, tensor.get(0));
+ assertEquals(4.0, tensor.get(4));
+ }
+
+ @Test
+ @DisplayName("Creates linspace tensor")
+ void testLinspace() {
+ Tensor tensor = Tensor.linspace(0, 10, 11);
+
+ assertArrayEquals(new int[]{11}, tensor.getShape());
+ assertEquals(0.0, tensor.get(0), 0.0001);
+ assertEquals(10.0, tensor.get(10), 0.0001);
+ assertEquals(5.0, tensor.get(5), 0.0001);
+ }
+
+ @Test
+ @DisplayName("Reshapes tensor")
+ void testReshape() {
+ Tensor tensor = new Tensor(new int[]{6}, new double[]{1, 2, 3, 4, 5, 6});
+ Tensor reshaped = tensor.reshape(new int[]{2, 3});
+
+ assertArrayEquals(new int[]{2, 3}, reshaped.getShape());
+ assertEquals(6, reshaped.getSize());
+ }
+
+ @Test
+ @DisplayName("Throws on invalid reshape")
+ void testInvalidReshape() {
+ Tensor tensor = Tensor.zeros(new int[]{4});
+
+ assertThrows(IllegalArgumentException.class, () -> {
+ tensor.reshape(new int[]{2, 3});
+ });
+ }
+
+ @Test
+ @DisplayName("Transposes 2D tensor")
+ void testTranspose() {
+ Tensor tensor = new Tensor(new int[]{2, 3}, new double[]{1, 2, 3, 4, 5, 6});
+ Tensor transposed = tensor.transpose();
+
+ assertArrayEquals(new int[]{3, 2}, transposed.getShape());
+ assertEquals(1.0, transposed.get(0, 0));
+ assertEquals(4.0, transposed.get(0, 1));
+ }
+
+ @Test
+ @DisplayName("Gets element at index")
+ void testGet() {
+ Tensor tensor = new Tensor(new int[]{2, 3}, new double[]{1, 2, 3, 4, 5, 6});
+
+ assertEquals(1.0, tensor.get(0, 0));
+ assertEquals(3.0, tensor.get(0, 2));
+ assertEquals(5.0, tensor.get(1, 1));
+ }
+
+ @Test
+ @DisplayName("Calculates sum")
+ void testSum() {
+ Tensor tensor = new Tensor(new int[]{4}, new double[]{1, 2, 3, 4});
+
+ assertEquals(10.0, tensor.sum());
+ }
+
+ @Test
+ @DisplayName("Calculates mean")
+ void testMean() {
+ Tensor tensor = new Tensor(new int[]{4}, new double[]{1, 2, 3, 4});
+
+ assertEquals(2.5, tensor.mean());
+ }
+
+ @Test
+ @DisplayName("Calculates std")
+ void testStd() {
+ Tensor tensor = new Tensor(new int[]{4}, new double[]{1, 2, 3, 4});
+
+ assertEquals(1.118, tensor.std(), 0.001);
+ }
+
+ @Test
+ @DisplayName("Finds min")
+ void testMin() {
+ Tensor tensor = new Tensor(new int[]{4}, new double[]{3, 1, 4, 2});
+
+ assertEquals(1.0, tensor.min());
+ }
+
+ @Test
+ @DisplayName("Finds max")
+ void testMax() {
+ Tensor tensor = new Tensor(new int[]{4}, new double[]{3, 1, 4, 2});
+
+ assertEquals(4.0, tensor.max());
+ }
+
+ @Test
+ @DisplayName("Applies ReLU activation")
+ void testRelu() {
+ Tensor tensor = new Tensor(new int[]{5}, new double[]{-2, -1, 0, 1, 2});
+ Tensor result = tensor.relu();
+
+ assertArrayEquals(new double[]{0, 0, 0, 1, 2}, result.getData(), 0.0001);
+ }
+
+ @Test
+ @DisplayName("Applies sigmoid activation")
+ void testSigmoid() {
+ Tensor tensor = new Tensor(new int[]{1}, new double[]{0});
+ Tensor result = tensor.sigmoid();
+
+ assertEquals(0.5, result.get(0), 0.0001);
+ }
+
+ @Test
+ @DisplayName("Applies softmax activation")
+ void testSoftmax() {
+ Tensor tensor = new Tensor(new int[]{3}, new double[]{1, 2, 3});
+ Tensor result = tensor.softmax();
+
+ assertEquals(1.0, result.sum(), 0.0001);
+ assertTrue(result.get(2) > result.get(1));
+ assertTrue(result.get(1) > result.get(0));
+ }
+
+ @Test
+ @DisplayName("Serializes and deserializes tensor")
+ void testSerialization() {
+ Tensor original = new Tensor(new int[]{2, 3}, new double[]{1, 2, 3, 4, 5, 6});
+ var json = original.toJson();
+
+ assertNotNull(json.get("shape"));
+ assertNotNull(json.get("data"));
+ assertNotNull(json.get("dtype"));
+
+ Tensor restored = Tensor.fromJson(json);
+ assertArrayEquals(original.getShape(), restored.getShape());
+ assertArrayEquals(original.getData(), restored.getData(), 0.0001);
+ }
+
+ @Test
+ @DisplayName("Converts to string representation")
+ void testToString() {
+ Tensor tensor = Tensor.zeros(new int[]{2, 3});
+ String str = tensor.toString();
+
+ assertTrue(str.contains("Tensor"));
+ assertTrue(str.contains("2, 3") || str.contains("[2,3]"));
+ }
+}
diff --git a/sdk/java/src/test/java/io/synor/compute/TypesTest.java b/sdk/java/src/test/java/io/synor/compute/TypesTest.java
new file mode 100644
index 0000000..e6040db
--- /dev/null
+++ b/sdk/java/src/test/java/io/synor/compute/TypesTest.java
@@ -0,0 +1,255 @@
+package io.synor.compute;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.DisplayName;
+import static org.junit.jupiter.api.Assertions.*;
+
+@DisplayName("Types Tests")
+class TypesTest {
+
+ @Test
+ @DisplayName("ProcessorType has all values")
+ void testProcessorTypeValues() {
+ assertEquals("cpu", ProcessorType.CPU.getValue());
+ assertEquals("gpu", ProcessorType.GPU.getValue());
+ assertEquals("tpu", ProcessorType.TPU.getValue());
+ assertEquals("npu", ProcessorType.NPU.getValue());
+ assertEquals("lpu", ProcessorType.LPU.getValue());
+ assertEquals("auto", ProcessorType.AUTO.getValue());
+ }
+
+ @Test
+ @DisplayName("ProcessorType parses from string")
+ void testProcessorTypeFromString() {
+ assertEquals(ProcessorType.GPU, ProcessorType.fromString("gpu"));
+ assertEquals(ProcessorType.TPU, ProcessorType.fromString("tpu"));
+ assertEquals(ProcessorType.AUTO, ProcessorType.fromString("invalid"));
+ }
+
+ @Test
+ @DisplayName("Precision has all values")
+ void testPrecisionValues() {
+ assertEquals("fp64", Precision.FP64.getValue());
+ assertEquals("fp32", Precision.FP32.getValue());
+ assertEquals("fp16", Precision.FP16.getValue());
+ assertEquals("bf16", Precision.BF16.getValue());
+ assertEquals("int8", Precision.INT8.getValue());
+ assertEquals("int4", Precision.INT4.getValue());
+ }
+
+ @Test
+ @DisplayName("Precision parses from string")
+ void testPrecisionFromString() {
+ assertEquals(Precision.FP16, Precision.fromString("fp16"));
+ assertEquals(Precision.BF16, Precision.fromString("bf16"));
+ assertEquals(Precision.FP32, Precision.fromString("invalid"));
+ }
+
+ @Test
+ @DisplayName("Priority has all values")
+ void testPriorityValues() {
+ assertEquals("critical", Priority.CRITICAL.getValue());
+ assertEquals("high", Priority.HIGH.getValue());
+ assertEquals("normal", Priority.NORMAL.getValue());
+ assertEquals("low", Priority.LOW.getValue());
+ assertEquals("background", Priority.BACKGROUND.getValue());
+ }
+
+ @Test
+ @DisplayName("JobStatus has all values")
+ void testJobStatusValues() {
+ assertEquals("pending", JobStatus.PENDING.getValue());
+ assertEquals("queued", JobStatus.QUEUED.getValue());
+ assertEquals("running", JobStatus.RUNNING.getValue());
+ assertEquals("completed", JobStatus.COMPLETED.getValue());
+ assertEquals("failed", JobStatus.FAILED.getValue());
+ assertEquals("cancelled", JobStatus.CANCELLED.getValue());
+ }
+
+ @Test
+ @DisplayName("JobStatus identifies terminal states")
+ void testJobStatusTerminal() {
+ assertTrue(JobStatus.COMPLETED.isTerminal());
+ assertTrue(JobStatus.FAILED.isTerminal());
+ assertTrue(JobStatus.CANCELLED.isTerminal());
+ assertFalse(JobStatus.PENDING.isTerminal());
+ assertFalse(JobStatus.RUNNING.isTerminal());
+ }
+
+ @Test
+ @DisplayName("ModelCategory has all values")
+ void testModelCategoryValues() {
+ assertEquals("llm", ModelCategory.LLM.getValue());
+ assertEquals("embedding", ModelCategory.EMBEDDING.getValue());
+ assertEquals("image_generation", ModelCategory.IMAGE_GENERATION.getValue());
+ assertEquals("custom", ModelCategory.CUSTOM.getValue());
+ }
+
+ @Test
+ @DisplayName("SynorConfig creates with defaults")
+ void testSynorConfigDefaults() {
+ SynorConfig config = SynorConfig.builder()
+ .apiKey("test-key")
+ .build();
+
+ assertEquals("test-key", config.getApiKey());
+ assertEquals("https://api.synor.io/compute/v1", config.getBaseUrl());
+ assertEquals(30, config.getTimeoutSeconds());
+ assertEquals(ProcessorType.AUTO, config.getDefaultProcessor());
+ assertEquals(Precision.FP32, config.getDefaultPrecision());
+ }
+
+ @Test
+ @DisplayName("SynorConfig creates with custom values")
+ void testSynorConfigCustom() {
+ SynorConfig config = SynorConfig.builder()
+ .apiKey("test-key")
+ .baseUrl("https://custom.api.com")
+ .timeoutSeconds(60)
+ .defaultProcessor(ProcessorType.GPU)
+ .defaultPrecision(Precision.FP16)
+ .debug(true)
+ .build();
+
+ assertEquals("https://custom.api.com", config.getBaseUrl());
+ assertEquals(60, config.getTimeoutSeconds());
+ assertEquals(ProcessorType.GPU, config.getDefaultProcessor());
+ assertTrue(config.isDebug());
+ }
+
+ @Test
+ @DisplayName("MatMulOptions generates correct JSON")
+ void testMatMulOptions() {
+ MatMulOptions options = MatMulOptions.builder()
+ .precision(Precision.FP16)
+ .processor(ProcessorType.GPU)
+ .priority(Priority.HIGH)
+ .build();
+
+ var json = options.toJson();
+ assertEquals("fp16", json.get("precision"));
+ assertEquals("gpu", json.get("processor"));
+ assertEquals("high", json.get("priority"));
+ }
+
+ @Test
+ @DisplayName("Conv2dOptions has correct defaults")
+ void testConv2dOptionsDefaults() {
+ Conv2dOptions options = Conv2dOptions.builder().build();
+
+ assertArrayEquals(new int[]{1, 1}, options.getStride());
+ assertArrayEquals(new int[]{0, 0}, options.getPadding());
+ }
+
+ @Test
+ @DisplayName("AttentionOptions generates correct JSON")
+ void testAttentionOptions() {
+ AttentionOptions options = AttentionOptions.builder()
+ .numHeads(8)
+ .flash(true)
+ .precision(Precision.FP16)
+ .build();
+
+ var json = options.toJson();
+ assertEquals(8, json.get("num_heads"));
+ assertEquals(true, json.get("flash"));
+ assertEquals("fp16", json.get("precision"));
+ }
+
+ @Test
+ @DisplayName("InferenceOptions has correct defaults")
+ void testInferenceOptionsDefaults() {
+ InferenceOptions options = InferenceOptions.builder().build();
+
+ assertEquals(256, options.getMaxTokens());
+ assertEquals(0.7, options.getTemperature(), 0.001);
+ assertEquals(0.9, options.getTopP(), 0.001);
+ assertEquals(50, options.getTopK());
+ }
+
+ @Test
+ @DisplayName("InferenceOptions generates correct JSON")
+ void testInferenceOptionsJson() {
+ InferenceOptions options = InferenceOptions.builder()
+ .maxTokens(512)
+ .temperature(0.8)
+ .topK(40)
+ .build();
+
+ var json = options.toJson();
+ assertEquals(512, json.get("max_tokens"));
+ assertEquals(0.8, ((Number)json.get("temperature")).doubleValue(), 0.001);
+ assertEquals(40, json.get("top_k"));
+ }
+
+ @Test
+ @DisplayName("JobResult identifies success")
+ void testJobResultSuccess() {
+ JobResult success = JobResult.builder()
+ .jobId("job-123")
+ .status(JobStatus.COMPLETED)
+ .result("output")
+ .build();
+
+ assertTrue(success.isSuccess());
+ assertFalse(success.isFailed());
+ }
+
+ @Test
+ @DisplayName("JobResult identifies failure")
+ void testJobResultFailure() {
+ JobResult failure = JobResult.builder()
+ .jobId("job-456")
+ .status(JobStatus.FAILED)
+ .error("Error message")
+ .build();
+
+ assertFalse(failure.isSuccess());
+ assertTrue(failure.isFailed());
+ }
+
+ @Test
+ @DisplayName("ModelInfo formats parameters correctly")
+ void testModelInfoFormattedParameters() {
+ ModelInfo model = ModelInfo.builder()
+ .id("test")
+ .name("Test Model")
+ .parameters(70000000000L)
+ .build();
+
+ assertEquals("70B", model.getFormattedParameters());
+ }
+
+ @Test
+ @DisplayName("PricingInfo parses from JSON")
+ void testPricingInfoFromJson() {
+ var json = new java.util.HashMap();
+ json.put("processor", "gpu");
+ json.put("price_per_second", 0.0001);
+ json.put("available_units", 100);
+ json.put("utilization_percent", 75.5);
+
+ PricingInfo pricing = PricingInfo.fromJson(json);
+
+ assertEquals("gpu", pricing.getProcessor());
+ assertEquals(0.0001, pricing.getPricePerSecond(), 0.00001);
+ assertEquals(100, pricing.getAvailableUnits());
+ }
+
+ @Test
+ @DisplayName("UsageStats parses from JSON")
+ void testUsageStatsFromJson() {
+ var json = new java.util.HashMap();
+ json.put("total_jobs", 1000);
+ json.put("completed_jobs", 950);
+ json.put("failed_jobs", 50);
+ json.put("total_compute_seconds", 3600.0);
+ json.put("total_cost", 0.36);
+
+ UsageStats stats = UsageStats.fromJson(json);
+
+ assertEquals(1000, stats.getTotalJobs());
+ assertEquals(950, stats.getCompletedJobs());
+ assertEquals(3600.0, stats.getTotalComputeSeconds(), 0.001);
+ }
+}
diff --git a/sdk/js/src/__tests__/client.test.ts b/sdk/js/src/__tests__/client.test.ts
new file mode 100644
index 0000000..4f3ad3a
--- /dev/null
+++ b/sdk/js/src/__tests__/client.test.ts
@@ -0,0 +1,369 @@
+/**
+ * Unit tests for SynorCompute client
+ */
+
+import { describe, it, expect, vi, beforeEach } from 'vitest';
+import { SynorCompute } from '../client';
+import { Tensor } from '../tensor';
+
+// Mock fetch for API calls
+const mockFetch = vi.fn();
+global.fetch = mockFetch;
+
+describe('SynorCompute Client', () => {
+ let client: SynorCompute;
+
+ beforeEach(() => {
+ mockFetch.mockReset();
+ client = new SynorCompute({ apiKey: 'test-api-key' });
+ });
+
+ describe('initialization', () => {
+ it('should create client with API key', () => {
+ const c = new SynorCompute({ apiKey: 'my-key' });
+ expect(c).toBeInstanceOf(SynorCompute);
+ });
+
+ it('should create client with config object', () => {
+ const c = new SynorCompute({
+ apiKey: 'my-key',
+ baseUrl: 'https://custom.api.com',
+ timeout: 60000,
+ });
+ expect(c).toBeInstanceOf(SynorCompute);
+ });
+
+ it('should throw without API key', () => {
+ expect(() => new SynorCompute({ apiKey: '' })).toThrow();
+ });
+ });
+
+ describe('matmul', () => {
+ it('should send matmul request', async () => {
+ mockFetch.mockResolvedValueOnce({
+ ok: true,
+ json: () => Promise.resolve({
+ jobId: 'job-123',
+ status: 'completed',
+ result: { data: 'base64result', shape: [2, 4], dtype: 'fp32' },
+ executionTimeMs: 15,
+ processor: 'gpu',
+ }),
+ });
+
+ const a = Tensor.random([2, 3]);
+ const b = Tensor.random([3, 4]);
+ const result = await client.matmul({ a, b });
+
+ expect(mockFetch).toHaveBeenCalledTimes(1);
+ expect(result.status).toBe('completed');
+ expect(result.jobId).toBe('job-123');
+ });
+
+ it('should support precision option', async () => {
+ mockFetch.mockResolvedValueOnce({
+ ok: true,
+ json: () => Promise.resolve({
+ jobId: 'job-456',
+ status: 'completed',
+ result: { data: 'base64', shape: [2, 2], dtype: 'fp16' },
+ }),
+ });
+
+ const a = Tensor.random([2, 2]);
+ const b = Tensor.random([2, 2]);
+ await client.matmul({ a, b, precision: 'fp16' });
+
+ const [url, options] = mockFetch.mock.calls[0];
+ const body = JSON.parse(options.body);
+ expect(body.precision).toBe('fp16');
+ });
+
+ it('should support processor selection', async () => {
+ mockFetch.mockResolvedValueOnce({
+ ok: true,
+ json: () => Promise.resolve({
+ jobId: 'job-789',
+ status: 'completed',
+ result: { data: 'base64', shape: [2, 2], dtype: 'fp32' },
+ }),
+ });
+
+ const a = Tensor.random([2, 2]);
+ const b = Tensor.random([2, 2]);
+ await client.matmul({ a, b, processor: 'tpu' });
+
+ const [url, options] = mockFetch.mock.calls[0];
+ const body = JSON.parse(options.body);
+ expect(body.processor).toBe('tpu');
+ });
+ });
+
+ describe('conv2d', () => {
+ it('should send conv2d request', async () => {
+ mockFetch.mockResolvedValueOnce({
+ ok: true,
+ json: () => Promise.resolve({
+ jobId: 'conv-123',
+ status: 'completed',
+ result: { data: 'base64', shape: [1, 32, 30, 30], dtype: 'fp32' },
+ }),
+ });
+
+ const input = Tensor.random([1, 3, 32, 32]);
+ const kernel = Tensor.random([32, 3, 3, 3]);
+ const result = await client.conv2d({ input, kernel });
+
+ expect(mockFetch).toHaveBeenCalledTimes(1);
+ expect(result.status).toBe('completed');
+ });
+
+ it('should support stride and padding options', async () => {
+ mockFetch.mockResolvedValueOnce({
+ ok: true,
+ json: () => Promise.resolve({
+ jobId: 'conv-456',
+ status: 'completed',
+ result: { data: 'base64', shape: [1, 32, 16, 16], dtype: 'fp32' },
+ }),
+ });
+
+ const input = Tensor.random([1, 3, 32, 32]);
+ const kernel = Tensor.random([32, 3, 3, 3]);
+ await client.conv2d({
+ input,
+ kernel,
+ stride: [2, 2],
+ padding: [1, 1],
+ });
+
+ const [url, options] = mockFetch.mock.calls[0];
+ const body = JSON.parse(options.body);
+ expect(body.stride).toEqual([2, 2]);
+ expect(body.padding).toEqual([1, 1]);
+ });
+ });
+
+ describe('attention', () => {
+ it('should send attention request', async () => {
+ mockFetch.mockResolvedValueOnce({
+ ok: true,
+ json: () => Promise.resolve({
+ jobId: 'attn-123',
+ status: 'completed',
+ result: { data: 'base64', shape: [1, 8, 64, 64], dtype: 'fp16' },
+ }),
+ });
+
+ const query = Tensor.random([1, 8, 64, 64]);
+ const key = Tensor.random([1, 8, 64, 64]);
+ const value = Tensor.random([1, 8, 64, 64]);
+ const result = await client.attention({ query, key, value });
+
+ expect(mockFetch).toHaveBeenCalledTimes(1);
+ expect(result.status).toBe('completed');
+ });
+
+ it('should support flash attention option', async () => {
+ mockFetch.mockResolvedValueOnce({
+ ok: true,
+ json: () => Promise.resolve({
+ jobId: 'attn-456',
+ status: 'completed',
+ result: { data: 'base64', shape: [1, 8, 64, 64], dtype: 'fp16' },
+ }),
+ });
+
+ const query = Tensor.random([1, 8, 64, 64]);
+ const key = Tensor.random([1, 8, 64, 64]);
+ const value = Tensor.random([1, 8, 64, 64]);
+ await client.attention({ query, key, value, flash: true });
+
+ const [url, options] = mockFetch.mock.calls[0];
+ const body = JSON.parse(options.body);
+ expect(body.flash).toBe(true);
+ });
+ });
+
+ describe('inference', () => {
+ it('should send inference request', async () => {
+ mockFetch.mockResolvedValueOnce({
+ ok: true,
+ json: () => Promise.resolve({
+ jobId: 'inf-123',
+ status: 'completed',
+ result: 'The answer to your question is...',
+ executionTimeMs: 2500,
+ processor: 'gpu',
+ }),
+ });
+
+ const result = await client.inference({
+ model: 'llama-3-70b',
+ prompt: 'What is the capital of France?',
+ });
+
+ expect(mockFetch).toHaveBeenCalledTimes(1);
+ expect(result.status).toBe('completed');
+ expect(result.result).toContain('answer');
+ });
+
+ it('should support inference options', async () => {
+ mockFetch.mockResolvedValueOnce({
+ ok: true,
+ json: () => Promise.resolve({
+ jobId: 'inf-456',
+ status: 'completed',
+ result: 'Generated text...',
+ }),
+ });
+
+ await client.inference({
+ model: 'llama-3-70b',
+ prompt: 'Hello',
+ maxTokens: 512,
+ temperature: 0.8,
+ topP: 0.95,
+ topK: 40,
+ });
+
+ const [url, options] = mockFetch.mock.calls[0];
+ const body = JSON.parse(options.body);
+ expect(body.maxTokens).toBe(512);
+ expect(body.temperature).toBe(0.8);
+ expect(body.topP).toBe(0.95);
+ expect(body.topK).toBe(40);
+ });
+ });
+
+ describe('model registry', () => {
+ it('should list models', async () => {
+ mockFetch.mockResolvedValueOnce({
+ ok: true,
+ json: () => Promise.resolve({
+ models: [
+ { id: 'llama-3-70b', name: 'Llama 3 70B', category: 'llm' },
+ { id: 'mistral-7b', name: 'Mistral 7B', category: 'llm' },
+ ],
+ }),
+ });
+
+ const models = await client.listModels();
+ expect(models).toHaveLength(2);
+ expect(models[0].id).toBe('llama-3-70b');
+ });
+
+ it('should filter models by category', async () => {
+ mockFetch.mockResolvedValueOnce({
+ ok: true,
+ json: () => Promise.resolve({
+ models: [
+ { id: 'sd-xl', name: 'Stable Diffusion XL', category: 'image_generation' },
+ ],
+ }),
+ });
+
+ await client.listModels({ category: 'image_generation' });
+
+ const [url] = mockFetch.mock.calls[0];
+ expect(url).toContain('category=image_generation');
+ });
+
+ it('should get model by ID', async () => {
+ mockFetch.mockResolvedValueOnce({
+ ok: true,
+ json: () => Promise.resolve({
+ id: 'llama-3-70b',
+ name: 'Llama 3 70B',
+ category: 'llm',
+ parameters: 70000000000,
+ contextLength: 8192,
+ }),
+ });
+
+ const model = await client.getModel('llama-3-70b');
+ expect(model.id).toBe('llama-3-70b');
+ expect(model.parameters).toBe(70000000000);
+ });
+ });
+
+ describe('pricing and usage', () => {
+ it('should get pricing', async () => {
+ mockFetch.mockResolvedValueOnce({
+ ok: true,
+ json: () => Promise.resolve({
+ pricing: [
+ { processor: 'gpu', pricePerSecond: 0.0001, availableUnits: 100 },
+ { processor: 'tpu', pricePerSecond: 0.0002, availableUnits: 50 },
+ ],
+ }),
+ });
+
+ const pricing = await client.getPricing();
+ expect(pricing).toHaveLength(2);
+ expect(pricing[0].processor).toBe('gpu');
+ });
+
+ it('should get usage', async () => {
+ mockFetch.mockResolvedValueOnce({
+ ok: true,
+ json: () => Promise.resolve({
+ totalJobs: 1000,
+ completedJobs: 950,
+ failedJobs: 50,
+ totalComputeSeconds: 3600,
+ totalCost: 0.36,
+ }),
+ });
+
+ const usage = await client.getUsage();
+ expect(usage.totalJobs).toBe(1000);
+ expect(usage.completedJobs).toBe(950);
+ });
+ });
+
+ describe('error handling', () => {
+ it('should handle API errors', async () => {
+ mockFetch.mockResolvedValueOnce({
+ ok: false,
+ status: 401,
+ json: () => Promise.resolve({ error: 'Invalid API key' }),
+ });
+
+ const a = Tensor.random([2, 2]);
+ const b = Tensor.random([2, 2]);
+
+ await expect(client.matmul({ a, b })).rejects.toThrow();
+ });
+
+ it('should handle network errors', async () => {
+ mockFetch.mockRejectedValueOnce(new Error('Network error'));
+
+ const a = Tensor.random([2, 2]);
+ const b = Tensor.random([2, 2]);
+
+ await expect(client.matmul({ a, b })).rejects.toThrow('Network error');
+ });
+ });
+
+ describe('health check', () => {
+ it('should return true for healthy service', async () => {
+ mockFetch.mockResolvedValueOnce({
+ ok: true,
+ json: () => Promise.resolve({ status: 'healthy' }),
+ });
+
+ const healthy = await client.healthCheck();
+ expect(healthy).toBe(true);
+ });
+
+ it('should return false for unhealthy service', async () => {
+ mockFetch.mockResolvedValueOnce({
+ ok: false,
+ });
+
+ const healthy = await client.healthCheck();
+ expect(healthy).toBe(false);
+ });
+ });
+});
diff --git a/sdk/js/src/__tests__/tensor.test.ts b/sdk/js/src/__tests__/tensor.test.ts
new file mode 100644
index 0000000..c12b64c
--- /dev/null
+++ b/sdk/js/src/__tests__/tensor.test.ts
@@ -0,0 +1,175 @@
+/**
+ * Unit tests for Tensor class
+ */
+
+import { describe, it, expect } from 'vitest';
+import { Tensor } from '../tensor';
+
+describe('Tensor', () => {
+ describe('creation', () => {
+ it('should create tensor from nested array', () => {
+ const t = Tensor.from([[1, 2, 3], [4, 5, 6]]);
+ expect(t.shape).toEqual([2, 3]);
+ expect(t.size).toBe(6);
+ expect(t.ndim).toBe(2);
+ });
+
+ it('should create tensor from flat array', () => {
+ const t = Tensor.from([1, 2, 3, 4]);
+ expect(t.shape).toEqual([4]);
+ expect(t.size).toBe(4);
+ expect(t.ndim).toBe(1);
+ });
+
+ it('should create tensor from Float32Array', () => {
+ const data = new Float32Array([1.0, 2.0, 3.0]);
+ const t = Tensor.from(data);
+ expect(t.dtype).toBe('fp32');
+ expect(t.size).toBe(3);
+ });
+
+ it('should create tensor from Float64Array', () => {
+ const data = new Float64Array([1.0, 2.0, 3.0]);
+ const t = Tensor.from(data);
+ expect(t.dtype).toBe('fp64');
+ });
+ });
+
+ describe('factory methods', () => {
+ it('should create zeros tensor', () => {
+ const t = Tensor.zeros([2, 3]);
+ expect(t.shape).toEqual([2, 3]);
+ expect(t.size).toBe(6);
+ expect(Array.from(t.data).every(v => v === 0)).toBe(true);
+ });
+
+ it('should create ones tensor', () => {
+ const t = Tensor.ones([3, 2]);
+ expect(t.shape).toEqual([3, 2]);
+ expect(Array.from(t.data).every(v => v === 1)).toBe(true);
+ });
+
+ it('should create random tensor', () => {
+ const t = Tensor.random([10, 10]);
+ expect(t.shape).toEqual([10, 10]);
+ expect(t.size).toBe(100);
+ // Values should be in [0, 1)
+ expect(Array.from(t.data).every(v => v >= 0 && v < 1)).toBe(true);
+ });
+
+ it('should create randn tensor', () => {
+ const t = Tensor.randn([100]);
+ expect(t.shape).toEqual([100]);
+ // Check approximate normal distribution properties
+ const values = Array.from(t.data);
+ const mean = values.reduce((a, b) => a + b) / values.length;
+ expect(Math.abs(mean)).toBeLessThan(0.5); // Should be close to 0
+ });
+
+ it('should support different dtypes', () => {
+ expect(Tensor.zeros([2, 2], 'fp64').dtype).toBe('fp64');
+ expect(Tensor.zeros([2, 2], 'fp32').dtype).toBe('fp32');
+ expect(Tensor.zeros([2, 2], 'int8').dtype).toBe('int8');
+ });
+ });
+
+ describe('operations', () => {
+ it('should reshape tensor', () => {
+ const t = Tensor.from([1, 2, 3, 4, 5, 6]);
+ const reshaped = t.reshape([2, 3]);
+ expect(reshaped.shape).toEqual([2, 3]);
+ expect(reshaped.size).toBe(6);
+ });
+
+ it('should throw on invalid reshape', () => {
+ const t = Tensor.from([1, 2, 3, 4]);
+ expect(() => t.reshape([2, 3])).toThrow();
+ });
+
+ it('should convert dtype', () => {
+ const t = Tensor.from([1.5, 2.5, 3.5], 'fp32');
+ const converted = t.to('fp64');
+ expect(converted.dtype).toBe('fp64');
+ expect(converted.data).toBeInstanceOf(Float64Array);
+ });
+
+ it('should get element at index', () => {
+ const t = Tensor.from([[1, 2, 3], [4, 5, 6]]);
+ expect(t.get(0, 0)).toBe(1);
+ expect(t.get(0, 2)).toBe(3);
+ expect(t.get(1, 1)).toBe(5);
+ });
+
+ it('should set element at index', () => {
+ const t = Tensor.from([[1, 2], [3, 4]]);
+ t.set(99, 0, 1);
+ expect(t.get(0, 1)).toBe(99);
+ });
+
+ it('should convert to array', () => {
+ const t = Tensor.from([[1, 2], [3, 4]]);
+ const arr = t.toArray();
+ expect(arr).toEqual([[1, 2], [3, 4]]);
+ });
+ });
+
+ describe('properties', () => {
+ it('should calculate byteSize correctly', () => {
+ const fp32 = Tensor.zeros([10], 'fp32');
+ expect(fp32.byteSize).toBe(40); // 10 * 4 bytes
+
+ const fp64 = Tensor.zeros([10], 'fp64');
+ expect(fp64.byteSize).toBe(80); // 10 * 8 bytes
+ });
+ });
+
+ describe('serialization', () => {
+ it('should serialize and deserialize tensor', () => {
+ const original = Tensor.from([[1, 2, 3], [4, 5, 6]]);
+ const serialized = original.serialize();
+
+ expect(serialized).toHaveProperty('data');
+ expect(serialized).toHaveProperty('shape');
+ expect(serialized).toHaveProperty('dtype');
+ expect(serialized.shape).toEqual([2, 3]);
+
+ const restored = Tensor.deserialize(serialized);
+ expect(restored.shape).toEqual(original.shape);
+ expect(restored.dtype).toBe(original.dtype);
+ expect(Array.from(restored.data)).toEqual(Array.from(original.data));
+ });
+
+ it('should preserve dtype during serialization', () => {
+ const fp64Tensor = Tensor.zeros([3], 'fp64');
+ fp64Tensor.set(1.234567890123, 0);
+
+ const serialized = fp64Tensor.serialize();
+ const restored = Tensor.deserialize(serialized);
+
+ expect(restored.dtype).toBe('fp64');
+ });
+ });
+
+ describe('edge cases', () => {
+ it('should handle scalar-like tensor', () => {
+ const t = Tensor.from([42]);
+ expect(t.shape).toEqual([1]);
+ expect(t.size).toBe(1);
+ });
+
+ it('should handle large tensors', () => {
+ const t = Tensor.zeros([1000, 1000]);
+ expect(t.size).toBe(1000000);
+ });
+
+ it('should throw on out-of-bounds access', () => {
+ const t = Tensor.from([1, 2, 3]);
+ expect(() => t.get(5)).toThrow();
+ });
+
+ it('should throw on wrong number of indices', () => {
+ const t = Tensor.from([[1, 2], [3, 4]]);
+ expect(() => t.get(0)).toThrow();
+ });
+ });
+});
diff --git a/sdk/js/src/__tests__/types.test.ts b/sdk/js/src/__tests__/types.test.ts
new file mode 100644
index 0000000..4d3601c
--- /dev/null
+++ b/sdk/js/src/__tests__/types.test.ts
@@ -0,0 +1,109 @@
+/**
+ * Unit tests for SDK types
+ */
+
+import { describe, it, expect } from 'vitest';
+import type {
+ ProcessorType,
+ Precision,
+ BalancingStrategy,
+ TaskPriority,
+ JobStatus,
+ SynorConfig,
+ MatMulRequest,
+ InferenceRequest,
+} from '../types';
+
+describe('Types', () => {
+ describe('ProcessorType', () => {
+ it('should accept valid processor types', () => {
+ const processors: ProcessorType[] = [
+ 'cpu', 'gpu', 'tpu', 'npu', 'lpu', 'fpga', 'dsp', 'webgpu', 'wasm', 'auto'
+ ];
+ expect(processors).toHaveLength(10);
+ });
+ });
+
+ describe('Precision', () => {
+ it('should accept valid precision types', () => {
+ const precisions: Precision[] = [
+ 'fp64', 'fp32', 'fp16', 'bf16', 'int8', 'int4'
+ ];
+ expect(precisions).toHaveLength(6);
+ });
+ });
+
+ describe('BalancingStrategy', () => {
+ it('should accept valid balancing strategies', () => {
+ const strategies: BalancingStrategy[] = [
+ 'cost', 'latency', 'energy', 'throughput', 'auto'
+ ];
+ expect(strategies).toHaveLength(5);
+ });
+ });
+
+ describe('TaskPriority', () => {
+ it('should accept valid priority levels', () => {
+ const priorities: TaskPriority[] = [
+ 'critical', 'high', 'normal', 'low', 'background'
+ ];
+ expect(priorities).toHaveLength(5);
+ });
+ });
+
+ describe('JobStatus', () => {
+ it('should accept valid job statuses', () => {
+ const statuses: JobStatus[] = [
+ 'pending', 'queued', 'running', 'completed', 'failed', 'cancelled'
+ ];
+ expect(statuses).toHaveLength(6);
+ });
+ });
+
+ describe('SynorConfig', () => {
+ it('should create valid config object', () => {
+ const config: SynorConfig = {
+ apiKey: 'test-api-key',
+ baseUrl: 'https://api.synor.io/compute/v1',
+ timeout: 30000,
+ };
+ expect(config.apiKey).toBe('test-api-key');
+ expect(config.baseUrl).toBeDefined();
+ });
+
+ it('should allow optional fields', () => {
+ const config: SynorConfig = {
+ apiKey: 'test-api-key',
+ };
+ expect(config.apiKey).toBe('test-api-key');
+ });
+ });
+
+ describe('MatMulRequest', () => {
+ it('should create valid matmul request', () => {
+ const request: MatMulRequest = {
+ a: { data: 'base64', shape: [2, 3], dtype: 'fp32' },
+ b: { data: 'base64', shape: [3, 4], dtype: 'fp32' },
+ precision: 'fp16',
+ processor: 'gpu',
+ };
+ expect(request.a.shape).toEqual([2, 3]);
+ expect(request.b.shape).toEqual([3, 4]);
+ });
+ });
+
+ describe('InferenceRequest', () => {
+ it('should create valid inference request', () => {
+ const request: InferenceRequest = {
+ model: 'llama-3-70b',
+ prompt: 'Hello, world!',
+ maxTokens: 256,
+ temperature: 0.7,
+ topP: 0.9,
+ topK: 50,
+ };
+ expect(request.model).toBe('llama-3-70b');
+ expect(request.maxTokens).toBe(256);
+ });
+ });
+});
diff --git a/sdk/python/tests/__init__.py b/sdk/python/tests/__init__.py
new file mode 100644
index 0000000..154576f
--- /dev/null
+++ b/sdk/python/tests/__init__.py
@@ -0,0 +1 @@
+# Tests for Synor Compute SDK
diff --git a/sdk/python/tests/test_client.py b/sdk/python/tests/test_client.py
new file mode 100644
index 0000000..7c07df7
--- /dev/null
+++ b/sdk/python/tests/test_client.py
@@ -0,0 +1,382 @@
+"""Unit tests for SynorCompute client."""
+
+import pytest
+from unittest.mock import AsyncMock, MagicMock, patch
+import numpy as np
+
+from synor_compute import SynorCompute, SynorError, Tensor
+from synor_compute.types import (
+ ProcessorType,
+ Precision,
+ JobStatus,
+ SynorConfig,
+)
+
+
+@pytest.fixture
+def mock_response():
+ """Create a mock HTTP response."""
+ def _create_response(data, status=200):
+ response = AsyncMock()
+ response.status = status
+ response.json = AsyncMock(return_value=data)
+ response.text = AsyncMock(return_value=str(data))
+ return response
+ return _create_response
+
+
+@pytest.fixture
+def client():
+ """Create a test client."""
+ return SynorCompute(api_key="test-api-key")
+
+
+class TestClientInitialization:
+ """Tests for client initialization."""
+
+ def test_create_with_api_key(self):
+ """Should create client with API key."""
+ client = SynorCompute(api_key="my-key")
+ assert client is not None
+
+ def test_create_with_config(self):
+ """Should create client with config."""
+ config = SynorConfig(
+ api_key="my-key",
+ base_url="https://custom.api.com",
+ timeout=60,
+ )
+ client = SynorCompute(config=config)
+ assert client is not None
+
+ def test_raise_without_api_key(self):
+ """Should raise error without API key."""
+ with pytest.raises((ValueError, TypeError)):
+ SynorCompute(api_key="")
+
+
+class TestMatMul:
+ """Tests for matrix multiplication."""
+
+ @pytest.mark.asyncio
+ async def test_matmul_request(self, client, mock_response):
+ """Should send matmul request."""
+ response_data = {
+ "job_id": "job-123",
+ "status": "completed",
+ "result": {"data": "base64", "shape": [2, 4], "dtype": "fp32"},
+ "execution_time_ms": 15,
+ "processor": "gpu",
+ }
+
+ with patch.object(client, "_request", new_callable=AsyncMock) as mock_req:
+ mock_req.return_value = response_data
+
+ a = Tensor.random((2, 3))
+ b = Tensor.random((3, 4))
+ result = await client.matmul(a, b)
+
+ mock_req.assert_called_once()
+ assert result.status == JobStatus.COMPLETED
+ assert result.job_id == "job-123"
+
+ @pytest.mark.asyncio
+ async def test_matmul_with_precision(self, client):
+ """Should support precision option."""
+ with patch.object(client, "_request", new_callable=AsyncMock) as mock_req:
+ mock_req.return_value = {
+ "job_id": "job-456",
+ "status": "completed",
+ "result": {"data": "base64", "shape": [2, 2], "dtype": "fp16"},
+ }
+
+ a = Tensor.random((2, 2))
+ b = Tensor.random((2, 2))
+ await client.matmul(a, b, precision=Precision.FP16)
+
+ call_args = mock_req.call_args
+ assert call_args[1].get("precision") == "fp16" or \
+ call_args[0][1].get("precision") == "fp16"
+
+ @pytest.mark.asyncio
+ async def test_matmul_with_processor(self, client):
+ """Should support processor selection."""
+ with patch.object(client, "_request", new_callable=AsyncMock) as mock_req:
+ mock_req.return_value = {
+ "job_id": "job-789",
+ "status": "completed",
+ "result": {"data": "base64", "shape": [2, 2], "dtype": "fp32"},
+ }
+
+ a = Tensor.random((2, 2))
+ b = Tensor.random((2, 2))
+ await client.matmul(a, b, processor=ProcessorType.TPU)
+
+ mock_req.assert_called_once()
+
+
+class TestConv2d:
+ """Tests for 2D convolution."""
+
+ @pytest.mark.asyncio
+ async def test_conv2d_request(self, client):
+ """Should send conv2d request."""
+ with patch.object(client, "_request", new_callable=AsyncMock) as mock_req:
+ mock_req.return_value = {
+ "job_id": "conv-123",
+ "status": "completed",
+ "result": {"data": "base64", "shape": [1, 32, 30, 30], "dtype": "fp32"},
+ }
+
+ input_tensor = Tensor.random((1, 3, 32, 32))
+ kernel = Tensor.random((32, 3, 3, 3))
+ result = await client.conv2d(input_tensor, kernel)
+
+ assert result.status == JobStatus.COMPLETED
+
+ @pytest.mark.asyncio
+ async def test_conv2d_with_options(self, client):
+ """Should support stride and padding options."""
+ with patch.object(client, "_request", new_callable=AsyncMock) as mock_req:
+ mock_req.return_value = {
+ "job_id": "conv-456",
+ "status": "completed",
+ "result": {"data": "base64", "shape": [1, 32, 16, 16], "dtype": "fp32"},
+ }
+
+ input_tensor = Tensor.random((1, 3, 32, 32))
+ kernel = Tensor.random((32, 3, 3, 3))
+ await client.conv2d(
+ input_tensor,
+ kernel,
+ stride=(2, 2),
+ padding=(1, 1),
+ )
+
+ mock_req.assert_called_once()
+
+
+class TestAttention:
+ """Tests for attention operation."""
+
+ @pytest.mark.asyncio
+ async def test_attention_request(self, client):
+ """Should send attention request."""
+ with patch.object(client, "_request", new_callable=AsyncMock) as mock_req:
+ mock_req.return_value = {
+ "job_id": "attn-123",
+ "status": "completed",
+ "result": {"data": "base64", "shape": [1, 8, 64, 64], "dtype": "fp16"},
+ }
+
+ query = Tensor.random((1, 8, 64, 64))
+ key = Tensor.random((1, 8, 64, 64))
+ value = Tensor.random((1, 8, 64, 64))
+ result = await client.attention(query, key, value)
+
+ assert result.status == JobStatus.COMPLETED
+
+ @pytest.mark.asyncio
+ async def test_attention_flash(self, client):
+ """Should support flash attention option."""
+ with patch.object(client, "_request", new_callable=AsyncMock) as mock_req:
+ mock_req.return_value = {
+ "job_id": "attn-456",
+ "status": "completed",
+ "result": {"data": "base64", "shape": [1, 8, 64, 64], "dtype": "fp16"},
+ }
+
+ query = Tensor.random((1, 8, 64, 64))
+ key = Tensor.random((1, 8, 64, 64))
+ value = Tensor.random((1, 8, 64, 64))
+ await client.attention(query, key, value, flash=True)
+
+ mock_req.assert_called_once()
+
+
+class TestInference:
+ """Tests for LLM inference."""
+
+ @pytest.mark.asyncio
+ async def test_inference_request(self, client):
+ """Should send inference request."""
+ with patch.object(client, "_request", new_callable=AsyncMock) as mock_req:
+ mock_req.return_value = {
+ "job_id": "inf-123",
+ "status": "completed",
+ "result": "The answer to your question is...",
+ "execution_time_ms": 2500,
+ "processor": "gpu",
+ }
+
+ result = await client.inference(
+ model="llama-3-70b",
+ prompt="What is the capital of France?",
+ )
+
+ assert result.status == JobStatus.COMPLETED
+ assert "answer" in result.result
+
+ @pytest.mark.asyncio
+ async def test_inference_with_options(self, client):
+ """Should support inference options."""
+ with patch.object(client, "_request", new_callable=AsyncMock) as mock_req:
+ mock_req.return_value = {
+ "job_id": "inf-456",
+ "status": "completed",
+ "result": "Generated text...",
+ }
+
+ await client.inference(
+ model="llama-3-70b",
+ prompt="Hello",
+ max_tokens=512,
+ temperature=0.8,
+ top_p=0.95,
+ top_k=40,
+ )
+
+ mock_req.assert_called_once()
+
+
+class TestModelRegistry:
+ """Tests for model registry operations."""
+
+ @pytest.mark.asyncio
+ async def test_list_models(self, client):
+ """Should list available models."""
+ with patch.object(client, "_request", new_callable=AsyncMock) as mock_req:
+ mock_req.return_value = {
+ "models": [
+ {"id": "llama-3-70b", "name": "Llama 3 70B", "category": "llm"},
+ {"id": "mistral-7b", "name": "Mistral 7B", "category": "llm"},
+ ]
+ }
+
+ models = await client.list_models()
+
+ assert len(models) == 2
+ assert models[0]["id"] == "llama-3-70b"
+
+ @pytest.mark.asyncio
+ async def test_list_models_by_category(self, client):
+ """Should filter models by category."""
+ with patch.object(client, "_request", new_callable=AsyncMock) as mock_req:
+ mock_req.return_value = {
+ "models": [
+ {"id": "sd-xl", "name": "Stable Diffusion XL", "category": "image_generation"},
+ ]
+ }
+
+ models = await client.list_models(category="image_generation")
+
+ assert len(models) == 1
+ assert models[0]["category"] == "image_generation"
+
+ @pytest.mark.asyncio
+ async def test_get_model(self, client):
+ """Should get model by ID."""
+ with patch.object(client, "_request", new_callable=AsyncMock) as mock_req:
+ mock_req.return_value = {
+ "id": "llama-3-70b",
+ "name": "Llama 3 70B",
+ "category": "llm",
+ "parameters": 70000000000,
+ "context_length": 8192,
+ }
+
+ model = await client.get_model("llama-3-70b")
+
+ assert model["id"] == "llama-3-70b"
+ assert model["parameters"] == 70000000000
+
+
+class TestPricingAndUsage:
+ """Tests for pricing and usage APIs."""
+
+ @pytest.mark.asyncio
+ async def test_get_pricing(self, client):
+ """Should get pricing information."""
+ with patch.object(client, "_request", new_callable=AsyncMock) as mock_req:
+ mock_req.return_value = {
+ "pricing": [
+ {"processor": "gpu", "price_per_second": 0.0001, "available_units": 100},
+ {"processor": "tpu", "price_per_second": 0.0002, "available_units": 50},
+ ]
+ }
+
+ pricing = await client.get_pricing()
+
+ assert len(pricing) == 2
+ assert pricing[0]["processor"] == "gpu"
+
+ @pytest.mark.asyncio
+ async def test_get_usage(self, client):
+ """Should get usage statistics."""
+ with patch.object(client, "_request", new_callable=AsyncMock) as mock_req:
+ mock_req.return_value = {
+ "total_jobs": 1000,
+ "completed_jobs": 950,
+ "failed_jobs": 50,
+ "total_compute_seconds": 3600,
+ "total_cost": 0.36,
+ }
+
+ usage = await client.get_usage()
+
+ assert usage["total_jobs"] == 1000
+ assert usage["completed_jobs"] == 950
+
+
+class TestErrorHandling:
+ """Tests for error handling."""
+
+ @pytest.mark.asyncio
+ async def test_api_error(self, client):
+ """Should handle API errors."""
+ with patch.object(client, "_request", new_callable=AsyncMock) as mock_req:
+ mock_req.side_effect = SynorError("Invalid API key", status_code=401)
+
+ a = Tensor.random((2, 2))
+ b = Tensor.random((2, 2))
+
+ with pytest.raises(SynorError) as exc_info:
+ await client.matmul(a, b)
+
+ assert exc_info.value.status_code == 401
+
+ @pytest.mark.asyncio
+ async def test_network_error(self, client):
+ """Should handle network errors."""
+ with patch.object(client, "_request", new_callable=AsyncMock) as mock_req:
+ mock_req.side_effect = Exception("Network error")
+
+ a = Tensor.random((2, 2))
+ b = Tensor.random((2, 2))
+
+ with pytest.raises(Exception, match="Network error"):
+ await client.matmul(a, b)
+
+
+class TestHealthCheck:
+ """Tests for health check."""
+
+ @pytest.mark.asyncio
+ async def test_healthy_service(self, client):
+ """Should return True for healthy service."""
+ with patch.object(client, "_request", new_callable=AsyncMock) as mock_req:
+ mock_req.return_value = {"status": "healthy"}
+
+ healthy = await client.health_check()
+
+ assert healthy is True
+
+ @pytest.mark.asyncio
+ async def test_unhealthy_service(self, client):
+ """Should return False for unhealthy service."""
+ with patch.object(client, "_request", new_callable=AsyncMock) as mock_req:
+ mock_req.side_effect = Exception("Service unavailable")
+
+ healthy = await client.health_check()
+
+ assert healthy is False
diff --git a/sdk/python/tests/test_tensor.py b/sdk/python/tests/test_tensor.py
new file mode 100644
index 0000000..3e6ba6f
--- /dev/null
+++ b/sdk/python/tests/test_tensor.py
@@ -0,0 +1,239 @@
+"""Unit tests for Tensor class."""
+
+import pytest
+import numpy as np
+from synor_compute import Tensor
+from synor_compute.types import Precision
+
+
+class TestTensorCreation:
+ """Tests for tensor creation methods."""
+
+ def test_from_numpy_array(self):
+ """Should create tensor from numpy array."""
+ arr = np.array([[1, 2, 3], [4, 5, 6]], dtype=np.float32)
+ tensor = Tensor.from_numpy(arr)
+
+ assert tensor.shape == (2, 3)
+ assert tensor.size == 6
+ assert tensor.ndim == 2
+ assert tensor.dtype == Precision.FP32
+
+ def test_from_numpy_with_dtype(self):
+ """Should create tensor with specified dtype."""
+ arr = np.array([1, 2, 3])
+ tensor = Tensor.from_numpy(arr, dtype=Precision.FP16)
+
+ assert tensor.dtype == Precision.FP16
+
+ def test_infer_dtype_fp64(self):
+ """Should infer FP64 from float64 array."""
+ arr = np.array([1.0, 2.0], dtype=np.float64)
+ tensor = Tensor.from_numpy(arr)
+
+ assert tensor.dtype == Precision.FP64
+
+ def test_infer_dtype_int8(self):
+ """Should infer INT8 from int8 array."""
+ arr = np.array([1, 2, 3], dtype=np.int8)
+ tensor = Tensor.from_numpy(arr)
+
+ assert tensor.dtype == Precision.INT8
+
+
+class TestTensorFactoryMethods:
+ """Tests for tensor factory methods."""
+
+ def test_zeros(self):
+ """Should create tensor of zeros."""
+ tensor = Tensor.zeros((2, 3))
+
+ assert tensor.shape == (2, 3)
+ assert np.allclose(tensor.data, 0)
+
+ def test_zeros_with_dtype(self):
+ """Should create zeros tensor with specified dtype."""
+ tensor = Tensor.zeros((2, 2), dtype=Precision.FP64)
+
+ assert tensor.dtype == Precision.FP64
+ assert tensor.data.dtype == np.float64
+
+ def test_ones(self):
+ """Should create tensor of ones."""
+ tensor = Tensor.ones((3, 2))
+
+ assert tensor.shape == (3, 2)
+ assert np.allclose(tensor.data, 1)
+
+ def test_random(self):
+ """Should create tensor with random values in [0, 1)."""
+ tensor = Tensor.random((10, 10))
+
+ assert tensor.shape == (10, 10)
+ assert tensor.data.min() >= 0
+ assert tensor.data.max() < 1
+
+ def test_randn(self):
+ """Should create tensor with normal distribution."""
+ tensor = Tensor.randn((1000,))
+
+ # Check approximate normal distribution properties
+ assert abs(tensor.data.mean()) < 0.2 # Mean ~= 0
+ assert 0.8 < tensor.data.std() < 1.2 # Std ~= 1
+
+
+class TestTensorOperations:
+ """Tests for tensor operations."""
+
+ def test_reshape(self):
+ """Should reshape tensor."""
+ tensor = Tensor.from_numpy(np.arange(6))
+ reshaped = tensor.reshape((2, 3))
+
+ assert reshaped.shape == (2, 3)
+ assert reshaped.size == 6
+
+ def test_reshape_preserves_data(self):
+ """Should preserve data when reshaping."""
+ arr = np.array([1, 2, 3, 4, 5, 6])
+ tensor = Tensor.from_numpy(arr)
+ reshaped = tensor.reshape((2, 3))
+
+ assert reshaped.data[0, 0] == 1
+ assert reshaped.data[1, 2] == 6
+
+ def test_to_different_dtype(self):
+ """Should convert to different dtype."""
+ tensor = Tensor.from_numpy(np.array([1.5, 2.5], dtype=np.float32))
+ converted = tensor.to(Precision.FP64)
+
+ assert converted.dtype == Precision.FP64
+ assert converted.data.dtype == np.float64
+
+ def test_to_same_dtype_returns_self(self):
+ """Should return self when converting to same dtype."""
+ tensor = Tensor.from_numpy(np.array([1.0, 2.0], dtype=np.float32))
+ same = tensor.to(Precision.FP32)
+
+ assert same is tensor
+
+ def test_numpy_returns_underlying_array(self):
+ """Should return underlying numpy array."""
+ arr = np.array([1, 2, 3], dtype=np.float32)
+ tensor = Tensor.from_numpy(arr)
+
+ assert np.array_equal(tensor.numpy(), arr)
+
+
+class TestTensorProperties:
+ """Tests for tensor properties."""
+
+ def test_shape(self):
+ """Should return correct shape."""
+ tensor = Tensor.zeros((3, 4, 5))
+ assert tensor.shape == (3, 4, 5)
+
+ def test_size(self):
+ """Should return correct size."""
+ tensor = Tensor.zeros((3, 4, 5))
+ assert tensor.size == 60
+
+ def test_ndim(self):
+ """Should return correct number of dimensions."""
+ tensor = Tensor.zeros((3, 4, 5))
+ assert tensor.ndim == 3
+
+ def test_nbytes(self):
+ """Should return correct byte size."""
+ tensor = Tensor.zeros((10,), dtype=Precision.FP32)
+ assert tensor.nbytes == 40 # 10 * 4 bytes
+
+
+class TestTensorSerialization:
+ """Tests for tensor serialization."""
+
+ def test_serialize(self):
+ """Should serialize tensor to dict."""
+ tensor = Tensor.from_numpy(np.array([[1, 2], [3, 4]], dtype=np.float32))
+ serialized = tensor.serialize()
+
+ assert "data" in serialized
+ assert "shape" in serialized
+ assert "dtype" in serialized
+ assert serialized["shape"] == [2, 2]
+ assert serialized["dtype"] == "fp32"
+
+ def test_deserialize(self):
+ """Should deserialize tensor from dict."""
+ original = Tensor.from_numpy(np.array([[1, 2, 3], [4, 5, 6]], dtype=np.float32))
+ serialized = original.serialize()
+ restored = Tensor.deserialize(serialized)
+
+ assert restored.shape == original.shape
+ assert restored.dtype == original.dtype
+ assert np.array_equal(restored.data, original.data)
+
+ def test_serialize_deserialize_fp64(self):
+ """Should preserve FP64 precision."""
+ original = Tensor.from_numpy(np.array([1.234567890123456], dtype=np.float64))
+ serialized = original.serialize()
+ restored = Tensor.deserialize(serialized)
+
+ assert restored.dtype == Precision.FP64
+ assert np.allclose(restored.data, original.data)
+
+ def test_serialize_deserialize_large_tensor(self):
+ """Should handle large tensors."""
+ original = Tensor.random((100, 100))
+ serialized = original.serialize()
+ restored = Tensor.deserialize(serialized)
+
+ assert restored.shape == original.shape
+ assert np.allclose(restored.data, original.data)
+
+
+class TestTensorRepr:
+ """Tests for tensor string representations."""
+
+ def test_repr(self):
+ """Should return informative repr."""
+ tensor = Tensor.zeros((2, 3), dtype=Precision.FP32)
+ rep = repr(tensor)
+
+ assert "Tensor" in rep
+ assert "(2, 3)" in rep
+ assert "fp32" in rep
+
+ def test_str(self):
+ """Should return string with data."""
+ tensor = Tensor.from_numpy(np.array([1, 2, 3], dtype=np.float32))
+ s = str(tensor)
+
+ assert "Tensor" in s
+ assert "fp32" in s
+
+
+class TestTensorEdgeCases:
+ """Tests for edge cases."""
+
+ def test_scalar_like_tensor(self):
+ """Should handle 1-element tensor."""
+ tensor = Tensor.from_numpy(np.array([42], dtype=np.float32))
+
+ assert tensor.shape == (1,)
+ assert tensor.size == 1
+ assert tensor.ndim == 1
+
+ def test_empty_shape_dimension(self):
+ """Should handle tensors with zero-size dimensions."""
+ tensor = Tensor.zeros((0, 3))
+
+ assert tensor.shape == (0, 3)
+ assert tensor.size == 0
+
+ def test_high_dimensional_tensor(self):
+ """Should handle high-dimensional tensors."""
+ tensor = Tensor.zeros((2, 3, 4, 5, 6))
+
+ assert tensor.ndim == 5
+ assert tensor.size == 720
diff --git a/sdk/python/tests/test_types.py b/sdk/python/tests/test_types.py
new file mode 100644
index 0000000..67d2289
--- /dev/null
+++ b/sdk/python/tests/test_types.py
@@ -0,0 +1,212 @@
+"""Unit tests for SDK types."""
+
+import pytest
+from synor_compute.types import (
+ ProcessorType,
+ Precision,
+ BalancingStrategy,
+ TaskPriority,
+ JobStatus,
+ SynorConfig,
+ JobResult,
+ JobMetrics,
+ PricingInfo,
+)
+
+
+class TestProcessorType:
+ """Tests for ProcessorType enum."""
+
+ def test_all_processor_types(self):
+ """Should have all expected processor types."""
+ expected = ["cpu", "gpu", "tpu", "npu", "lpu", "fpga", "dsp", "webgpu", "wasm", "auto"]
+ actual = [p.value for p in ProcessorType]
+
+ for e in expected:
+ assert e in actual
+
+ def test_processor_value(self):
+ """Should have correct values."""
+ assert ProcessorType.GPU.value == "gpu"
+ assert ProcessorType.TPU.value == "tpu"
+ assert ProcessorType.AUTO.value == "auto"
+
+
+class TestPrecision:
+ """Tests for Precision enum."""
+
+ def test_all_precision_types(self):
+ """Should have all expected precision types."""
+ expected = ["fp64", "fp32", "fp16", "bf16", "int8", "int4"]
+ actual = [p.value for p in Precision]
+
+ for e in expected:
+ assert e in actual
+
+ def test_precision_value(self):
+ """Should have correct values."""
+ assert Precision.FP64.value == "fp64"
+ assert Precision.FP32.value == "fp32"
+ assert Precision.FP16.value == "fp16"
+
+
+class TestBalancingStrategy:
+ """Tests for BalancingStrategy enum."""
+
+ def test_all_strategies(self):
+ """Should have all expected strategies."""
+ expected = ["cost", "latency", "energy", "throughput", "auto"]
+ actual = [s.value for s in BalancingStrategy]
+
+ for e in expected:
+ assert e in actual
+
+
+class TestTaskPriority:
+ """Tests for TaskPriority enum."""
+
+ def test_all_priorities(self):
+ """Should have all expected priorities."""
+ expected = ["critical", "high", "normal", "low", "background"]
+ actual = [p.value for p in TaskPriority]
+
+ for e in expected:
+ assert e in actual
+
+
+class TestJobStatus:
+ """Tests for JobStatus enum."""
+
+ def test_all_statuses(self):
+ """Should have all expected statuses."""
+ expected = ["pending", "queued", "running", "completed", "failed", "cancelled"]
+ actual = [s.value for s in JobStatus]
+
+ for e in expected:
+ assert e in actual
+
+
+class TestSynorConfig:
+ """Tests for SynorConfig dataclass."""
+
+ def test_create_with_defaults(self):
+ """Should create config with defaults."""
+ config = SynorConfig(api_key="test-key")
+
+ assert config.api_key == "test-key"
+ assert config.base_url == "https://api.synor.io/compute/v1"
+ assert config.timeout == 30
+
+ def test_create_with_custom_values(self):
+ """Should create config with custom values."""
+ config = SynorConfig(
+ api_key="test-key",
+ base_url="https://custom.api.com",
+ timeout=60,
+ default_processor=ProcessorType.GPU,
+ default_precision=Precision.FP16,
+ )
+
+ assert config.base_url == "https://custom.api.com"
+ assert config.timeout == 60
+ assert config.default_processor == ProcessorType.GPU
+ assert config.default_precision == Precision.FP16
+
+
+class TestJobResult:
+ """Tests for JobResult dataclass."""
+
+ def test_create_job_result(self):
+ """Should create job result."""
+ result = JobResult(
+ job_id="job-123",
+ status=JobStatus.COMPLETED,
+ result={"data": "value"},
+ execution_time_ms=150,
+ processor=ProcessorType.GPU,
+ )
+
+ assert result.job_id == "job-123"
+ assert result.status == JobStatus.COMPLETED
+ assert result.execution_time_ms == 150
+
+ def test_is_success(self):
+ """Should identify successful job."""
+ success = JobResult(
+ job_id="job-1",
+ status=JobStatus.COMPLETED,
+ result="data",
+ )
+ failure = JobResult(
+ job_id="job-2",
+ status=JobStatus.FAILED,
+ error="Error message",
+ )
+
+ assert success.is_success()
+ assert not failure.is_success()
+
+ def test_is_failed(self):
+ """Should identify failed job."""
+ success = JobResult(
+ job_id="job-1",
+ status=JobStatus.COMPLETED,
+ result="data",
+ )
+ failure = JobResult(
+ job_id="job-2",
+ status=JobStatus.FAILED,
+ error="Error message",
+ )
+
+ assert not success.is_failed()
+ assert failure.is_failed()
+
+
+class TestJobMetrics:
+ """Tests for JobMetrics dataclass."""
+
+ def test_create_metrics(self):
+ """Should create job metrics."""
+ metrics = JobMetrics(
+ queue_time_ms=10,
+ compute_time_ms=100,
+ transfer_time_ms=5,
+ total_time_ms=115,
+ flops=1e12,
+ memory_used_mb=1024,
+ )
+
+ assert metrics.compute_time_ms == 100
+ assert metrics.flops == 1e12
+
+
+class TestPricingInfo:
+ """Tests for PricingInfo dataclass."""
+
+ def test_create_pricing_info(self):
+ """Should create pricing info."""
+ pricing = PricingInfo(
+ processor=ProcessorType.GPU,
+ price_per_second=0.0001,
+ available_units=100,
+ utilization_percent=75.5,
+ )
+
+ assert pricing.processor == ProcessorType.GPU
+ assert pricing.price_per_second == 0.0001
+ assert pricing.available_units == 100
+
+ def test_optional_fields(self):
+ """Should handle optional fields."""
+ pricing = PricingInfo(
+ processor=ProcessorType.TPU,
+ price_per_second=0.0002,
+ available_units=50,
+ utilization_percent=60.0,
+ aws_equivalent_price=0.0010,
+ savings_percent=80.0,
+ )
+
+ assert pricing.aws_equivalent_price == 0.0010
+ assert pricing.savings_percent == 80.0
diff --git a/sdk/ruby/test/test_tensor.rb b/sdk/ruby/test/test_tensor.rb
new file mode 100644
index 0000000..a2f8a13
--- /dev/null
+++ b/sdk/ruby/test/test_tensor.rb
@@ -0,0 +1,117 @@
+# frozen_string_literal: true
+
+require 'minitest/autorun'
+require_relative '../lib/synor_compute'
+
+class TestTensor < Minitest::Test
+ def test_creation
+ tensor = SynorCompute::Tensor.new([2, 3], [1.0, 2.0, 3.0, 4.0, 5.0, 6.0])
+ assert_equal [2, 3], tensor.shape
+ assert_equal 6, tensor.size
+ assert_equal 2, tensor.ndim
+ end
+
+ def test_zeros
+ tensor = SynorCompute::Tensor.zeros([3, 4])
+ assert_equal [3, 4], tensor.shape
+ assert_equal 12, tensor.size
+ assert tensor.data.all? { |v| v == 0.0 }
+ end
+
+ def test_ones
+ tensor = SynorCompute::Tensor.ones([2, 2])
+ assert tensor.data.all? { |v| v == 1.0 }
+ end
+
+ def test_random
+ tensor = SynorCompute::Tensor.rand([10, 10])
+ assert_equal [10, 10], tensor.shape
+ assert tensor.data.all? { |v| v >= 0 && v < 1 }
+ end
+
+ def test_randn
+ tensor = SynorCompute::Tensor.randn([1000])
+ mean = tensor.mean
+ std = tensor.std
+ assert mean.abs < 0.2, "Mean should be close to 0, got #{mean}"
+ assert std > 0.8 && std < 1.2, "Std should be close to 1, got #{std}"
+ end
+
+ def test_eye
+ tensor = SynorCompute::Tensor.eye(3)
+ assert_equal [3, 3], tensor.shape
+ assert_equal 1.0, tensor[[0, 0]]
+ assert_equal 1.0, tensor[[1, 1]]
+ assert_equal 0.0, tensor[[0, 1]]
+ end
+
+ def test_arange
+ tensor = SynorCompute::Tensor.arange(0, 5, 1)
+ assert_equal [5], tensor.shape
+ assert_equal 0.0, tensor[[0]]
+ assert_equal 4.0, tensor[[4]]
+ end
+
+ def test_linspace
+ tensor = SynorCompute::Tensor.linspace(0, 10, 11)
+ assert_equal [11], tensor.shape
+ assert_in_delta 0.0, tensor[[0]], 0.0001
+ assert_in_delta 10.0, tensor[[10]], 0.0001
+ end
+
+ def test_reshape
+ tensor = SynorCompute::Tensor.new([6], [1, 2, 3, 4, 5, 6])
+ reshaped = tensor.reshape([2, 3])
+ assert_equal [2, 3], reshaped.shape
+ assert_equal 6, reshaped.size
+ end
+
+ def test_transpose
+ tensor = SynorCompute::Tensor.new([2, 3], [1, 2, 3, 4, 5, 6])
+ transposed = tensor.transpose
+ assert_equal [3, 2], transposed.shape
+ end
+
+ def test_sum
+ tensor = SynorCompute::Tensor.new([4], [1, 2, 3, 4])
+ assert_equal 10.0, tensor.sum
+ end
+
+ def test_mean
+ tensor = SynorCompute::Tensor.new([4], [1, 2, 3, 4])
+ assert_equal 2.5, tensor.mean
+ end
+
+ def test_min_max
+ tensor = SynorCompute::Tensor.new([4], [3, 1, 4, 2])
+ assert_equal 1.0, tensor.min
+ assert_equal 4.0, tensor.max
+ end
+
+ def test_relu
+ tensor = SynorCompute::Tensor.new([5], [-2, -1, 0, 1, 2])
+ result = tensor.relu
+ assert_equal [0, 0, 0, 1, 2], result.data
+ end
+
+ def test_sigmoid
+ tensor = SynorCompute::Tensor.new([1], [0])
+ result = tensor.sigmoid
+ assert_in_delta 0.5, result[[0]], 0.0001
+ end
+
+ def test_softmax
+ tensor = SynorCompute::Tensor.new([3], [1, 2, 3])
+ result = tensor.softmax
+ assert_in_delta 1.0, result.sum, 0.0001
+ assert result[[2]] > result[[1]]
+ end
+
+ def test_serialization
+ original = SynorCompute::Tensor.new([2, 3], [1, 2, 3, 4, 5, 6])
+ json = original.to_json
+ restored = SynorCompute::Tensor.from_json(json)
+ assert_equal original.shape, restored.shape
+ assert_equal original.data, restored.data
+ end
+end
diff --git a/sdk/ruby/test/test_types.rb b/sdk/ruby/test/test_types.rb
new file mode 100644
index 0000000..d592e18
--- /dev/null
+++ b/sdk/ruby/test/test_types.rb
@@ -0,0 +1,102 @@
+# frozen_string_literal: true
+
+require 'minitest/autorun'
+require_relative '../lib/synor_compute'
+
+class TestTypes < Minitest::Test
+ def test_processor_type_values
+ assert_equal 'cpu', SynorCompute::ProcessorType::CPU
+ assert_equal 'gpu', SynorCompute::ProcessorType::GPU
+ assert_equal 'tpu', SynorCompute::ProcessorType::TPU
+ assert_equal 'npu', SynorCompute::ProcessorType::NPU
+ assert_equal 'auto', SynorCompute::ProcessorType::AUTO
+ end
+
+ def test_precision_values
+ assert_equal 'fp64', SynorCompute::Precision::FP64
+ assert_equal 'fp32', SynorCompute::Precision::FP32
+ assert_equal 'fp16', SynorCompute::Precision::FP16
+ assert_equal 'bf16', SynorCompute::Precision::BF16
+ assert_equal 'int8', SynorCompute::Precision::INT8
+ end
+
+ def test_priority_values
+ assert_equal 'critical', SynorCompute::Priority::CRITICAL
+ assert_equal 'high', SynorCompute::Priority::HIGH
+ assert_equal 'normal', SynorCompute::Priority::NORMAL
+ assert_equal 'low', SynorCompute::Priority::LOW
+ end
+
+ def test_job_status_values
+ assert_equal 'pending', SynorCompute::JobStatus::PENDING
+ assert_equal 'running', SynorCompute::JobStatus::RUNNING
+ assert_equal 'completed', SynorCompute::JobStatus::COMPLETED
+ assert_equal 'failed', SynorCompute::JobStatus::FAILED
+ end
+
+ def test_config_defaults
+ config = SynorCompute::Config.new(api_key: 'test-key')
+ assert_equal 'test-key', config.api_key
+ assert_equal 'https://api.synor.io/compute/v1', config.base_url
+ assert_equal 30, config.timeout
+ end
+
+ def test_config_custom
+ config = SynorCompute::Config.new(
+ api_key: 'test-key',
+ base_url: 'https://custom.api.com',
+ timeout: 60,
+ default_processor: SynorCompute::ProcessorType::GPU
+ )
+ assert_equal 'https://custom.api.com', config.base_url
+ assert_equal 60, config.timeout
+ end
+
+ def test_matmul_options_to_hash
+ options = SynorCompute::MatMulOptions.new(
+ precision: SynorCompute::Precision::FP16,
+ processor: SynorCompute::ProcessorType::GPU
+ )
+ hash = options.to_h
+ assert_equal 'fp16', hash[:precision]
+ assert_equal 'gpu', hash[:processor]
+ end
+
+ def test_inference_options_defaults
+ options = SynorCompute::InferenceOptions.new
+ assert_equal 256, options.max_tokens
+ assert_in_delta 0.7, options.temperature, 0.001
+ assert_in_delta 0.9, options.top_p, 0.001
+ assert_equal 50, options.top_k
+ end
+
+ def test_job_result_success
+ result = SynorCompute::JobResult.new(
+ job_id: 'job-123',
+ status: SynorCompute::JobStatus::COMPLETED,
+ result: 'output'
+ )
+ assert result.success?
+ refute result.failed?
+ end
+
+ def test_job_result_failure
+ result = SynorCompute::JobResult.new(
+ job_id: 'job-456',
+ status: SynorCompute::JobStatus::FAILED,
+ error: 'Error message'
+ )
+ refute result.success?
+ assert result.failed?
+ end
+
+ def test_model_info_formatted_parameters
+ model = SynorCompute::ModelInfo.new(
+ id: 'test',
+ name: 'Test Model',
+ category: 'llm',
+ parameters: 70_000_000_000
+ )
+ assert_equal '70B', model.formatted_parameters
+ end
+end
diff --git a/sdk/rust/src/lib.rs b/sdk/rust/src/lib.rs
index ee2e586..b4aad91 100644
--- a/sdk/rust/src/lib.rs
+++ b/sdk/rust/src/lib.rs
@@ -48,6 +48,9 @@ mod tensor;
mod client;
mod error;
+#[cfg(test)]
+mod tests;
+
pub use types::*;
pub use tensor::Tensor;
pub use client::SynorCompute;
diff --git a/sdk/rust/src/tests.rs b/sdk/rust/src/tests.rs
new file mode 100644
index 0000000..e65af36
--- /dev/null
+++ b/sdk/rust/src/tests.rs
@@ -0,0 +1,334 @@
+//! Comprehensive tests for Synor Compute Rust SDK.
+
+#[cfg(test)]
+mod tensor_tests {
+ use crate::tensor::Tensor;
+ use crate::types::Precision;
+
+ #[test]
+ fn test_tensor_creation() {
+ let t = Tensor::new(&[2, 3], vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0]);
+ assert_eq!(t.shape(), &[2, 3]);
+ assert_eq!(t.size(), 6);
+ assert_eq!(t.ndim(), 2);
+ }
+
+ #[test]
+ fn test_tensor_zeros() {
+ let t = Tensor::zeros(&[3, 3]);
+ assert_eq!(t.shape(), &[3, 3]);
+ assert!(t.data().iter().all(|&x| x == 0.0));
+ }
+
+ #[test]
+ fn test_tensor_ones() {
+ let t = Tensor::ones(&[2, 2]);
+ assert!(t.data().iter().all(|&x| x == 1.0));
+ }
+
+ #[test]
+ fn test_tensor_rand() {
+ let t = Tensor::rand(&[10, 10]);
+ assert_eq!(t.size(), 100);
+ assert!(t.data().iter().all(|&x| x >= 0.0 && x < 1.0));
+ }
+
+ #[test]
+ fn test_tensor_randn() {
+ let t = Tensor::randn(&[1000]);
+ let mean = t.mean();
+ let std = t.std();
+ assert!(mean.abs() < 0.2, "Mean should be close to 0");
+ assert!(std > 0.8 && std < 1.2, "Std should be close to 1");
+ }
+
+ #[test]
+ fn test_tensor_eye() {
+ let t = Tensor::eye(3);
+ assert_eq!(t.shape(), &[3, 3]);
+ assert_eq!(t.get(&[0, 0]), 1.0);
+ assert_eq!(t.get(&[1, 1]), 1.0);
+ assert_eq!(t.get(&[2, 2]), 1.0);
+ assert_eq!(t.get(&[0, 1]), 0.0);
+ }
+
+ #[test]
+ fn test_tensor_arange() {
+ let t = Tensor::arange(0.0, 5.0, 1.0);
+ assert_eq!(t.shape(), &[5]);
+ assert_eq!(t.data()[0], 0.0);
+ assert_eq!(t.data()[4], 4.0);
+ }
+
+ #[test]
+ fn test_tensor_linspace() {
+ let t = Tensor::linspace(0.0, 10.0, 11);
+ assert_eq!(t.shape(), &[11]);
+ assert!((t.data()[0] - 0.0).abs() < 1e-10);
+ assert!((t.data()[10] - 10.0).abs() < 1e-10);
+ }
+
+ #[test]
+ fn test_tensor_reshape() {
+ let t = Tensor::new(&[6], vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0]);
+ let reshaped = t.reshape(&[2, 3]);
+ assert_eq!(reshaped.shape(), &[2, 3]);
+ assert_eq!(reshaped.size(), 6);
+ }
+
+ #[test]
+ #[should_panic]
+ fn test_tensor_invalid_reshape() {
+ let t = Tensor::zeros(&[4]);
+ t.reshape(&[2, 3]); // Should panic
+ }
+
+ #[test]
+ fn test_tensor_transpose() {
+ let t = Tensor::new(&[2, 3], vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0]);
+ let transposed = t.transpose();
+ assert_eq!(transposed.shape(), &[3, 2]);
+ }
+
+ #[test]
+ fn test_tensor_mean() {
+ let t = Tensor::new(&[4], vec![1.0, 2.0, 3.0, 4.0]);
+ assert!((t.mean() - 2.5).abs() < 1e-10);
+ }
+
+ #[test]
+ fn test_tensor_sum() {
+ let t = Tensor::new(&[4], vec![1.0, 2.0, 3.0, 4.0]);
+ assert!((t.sum() - 10.0).abs() < 1e-10);
+ }
+
+ #[test]
+ fn test_tensor_std() {
+ let t = Tensor::new(&[4], vec![1.0, 2.0, 3.0, 4.0]);
+ assert!((t.std() - 1.118).abs() < 0.01);
+ }
+
+ #[test]
+ fn test_tensor_min_max() {
+ let t = Tensor::new(&[4], vec![3.0, 1.0, 4.0, 2.0]);
+ assert_eq!(t.min(), 1.0);
+ assert_eq!(t.max(), 4.0);
+ }
+
+ #[test]
+ fn test_tensor_relu() {
+ let t = Tensor::new(&[5], vec![-2.0, -1.0, 0.0, 1.0, 2.0]);
+ let result = t.relu();
+ assert_eq!(result.data(), &[0.0, 0.0, 0.0, 1.0, 2.0]);
+ }
+
+ #[test]
+ fn test_tensor_sigmoid() {
+ let t = Tensor::new(&[1], vec![0.0]);
+ let result = t.sigmoid();
+ assert!((result.data()[0] - 0.5).abs() < 1e-10);
+ }
+
+ #[test]
+ fn test_tensor_softmax() {
+ let t = Tensor::new(&[3], vec![1.0, 2.0, 3.0]);
+ let result = t.softmax();
+ assert!((result.sum() - 1.0).abs() < 1e-10);
+ assert!(result.data()[2] > result.data()[1]);
+ }
+
+ #[test]
+ fn test_tensor_dtype() {
+ let t = Tensor::zeros(&[2, 2]).with_dtype(Precision::FP16);
+ assert_eq!(t.dtype(), Precision::FP16);
+ }
+}
+
+#[cfg(test)]
+mod types_tests {
+ use crate::types::*;
+
+ #[test]
+ fn test_processor_type_values() {
+ assert_eq!(ProcessorType::Cpu.as_str(), "cpu");
+ assert_eq!(ProcessorType::Gpu.as_str(), "gpu");
+ assert_eq!(ProcessorType::Tpu.as_str(), "tpu");
+ assert_eq!(ProcessorType::Npu.as_str(), "npu");
+ assert_eq!(ProcessorType::Lpu.as_str(), "lpu");
+ assert_eq!(ProcessorType::Auto.as_str(), "auto");
+ }
+
+ #[test]
+ fn test_precision_values() {
+ assert_eq!(Precision::FP64.as_str(), "fp64");
+ assert_eq!(Precision::FP32.as_str(), "fp32");
+ assert_eq!(Precision::FP16.as_str(), "fp16");
+ assert_eq!(Precision::BF16.as_str(), "bf16");
+ assert_eq!(Precision::INT8.as_str(), "int8");
+ assert_eq!(Precision::INT4.as_str(), "int4");
+ }
+
+ #[test]
+ fn test_priority_default() {
+ let priority = Priority::default();
+ assert_eq!(priority, Priority::Normal);
+ }
+
+ #[test]
+ fn test_job_status_default() {
+ let status = JobStatus::default();
+ assert_eq!(status, JobStatus::Pending);
+ }
+
+ #[test]
+ fn test_config_builder() {
+ let config = Config::new("test-api-key");
+ assert_eq!(config.api_key, "test-api-key");
+ assert_eq!(config.base_url, "https://api.synor.io/compute/v1");
+ assert_eq!(config.timeout_secs, 30);
+ assert!(!config.debug);
+ }
+
+ #[test]
+ fn test_config_custom() {
+ let config = Config::new("test-key")
+ .base_url("https://custom.api.com")
+ .default_processor(ProcessorType::Gpu)
+ .default_precision(Precision::FP16)
+ .timeout_secs(60)
+ .debug(true);
+
+ assert_eq!(config.base_url, "https://custom.api.com");
+ assert_eq!(config.default_processor, ProcessorType::Gpu);
+ assert_eq!(config.default_precision, Precision::FP16);
+ assert_eq!(config.timeout_secs, 60);
+ assert!(config.debug);
+ }
+
+ #[test]
+ fn test_matmul_options_default() {
+ let options = MatMulOptions::default();
+ assert_eq!(options.precision, Precision::FP32);
+ assert_eq!(options.processor, ProcessorType::Auto);
+ assert_eq!(options.priority, Priority::Normal);
+ }
+
+ #[test]
+ fn test_conv2d_options_default() {
+ let options = Conv2dOptions::default();
+ assert_eq!(options.stride, (1, 1));
+ assert_eq!(options.padding, (0, 0));
+ }
+
+ #[test]
+ fn test_attention_options_default() {
+ let options = AttentionOptions::default();
+ assert_eq!(options.num_heads, 8);
+ assert!(options.flash);
+ assert_eq!(options.precision, Precision::FP16);
+ }
+
+ #[test]
+ fn test_inference_options_default() {
+ let options = InferenceOptions::default();
+ assert_eq!(options.max_tokens, 256);
+ assert!((options.temperature - 0.7).abs() < 0.001);
+ assert!((options.top_p - 0.9).abs() < 0.001);
+ assert_eq!(options.top_k, 50);
+ }
+
+ #[test]
+ fn test_job_result_success() {
+ let result: JobResult = JobResult {
+ job_id: Some("job-123".to_string()),
+ status: JobStatus::Completed,
+ result: Some("output".to_string()),
+ error: None,
+ execution_time_ms: Some(100),
+ processor: Some(ProcessorType::Gpu),
+ cost: Some(0.01),
+ };
+
+ assert!(result.is_success());
+ assert!(!result.is_failed());
+ }
+
+ #[test]
+ fn test_job_result_failure() {
+ let result: JobResult = JobResult {
+ job_id: Some("job-456".to_string()),
+ status: JobStatus::Failed,
+ result: None,
+ error: Some("Error message".to_string()),
+ execution_time_ms: None,
+ processor: None,
+ cost: None,
+ };
+
+ assert!(!result.is_success());
+ assert!(result.is_failed());
+ }
+
+ #[test]
+ fn test_model_info_formatted_parameters() {
+ let model = ModelInfo {
+ id: "test".to_string(),
+ name: "Test Model".to_string(),
+ description: None,
+ category: "llm".to_string(),
+ parameters: Some(70_000_000_000),
+ context_length: Some(8192),
+ format: None,
+ recommended_processor: None,
+ license: None,
+ cid: None,
+ };
+
+ assert_eq!(model.formatted_parameters(), "70B");
+ }
+
+ #[test]
+ fn test_model_info_formatted_parameters_millions() {
+ let model = ModelInfo {
+ id: "test".to_string(),
+ name: "Test Model".to_string(),
+ description: None,
+ category: "embedding".to_string(),
+ parameters: Some(350_000_000),
+ context_length: None,
+ format: None,
+ recommended_processor: None,
+ license: None,
+ cid: None,
+ };
+
+ assert_eq!(model.formatted_parameters(), "350M");
+ }
+}
+
+#[cfg(test)]
+mod error_tests {
+ use crate::error::Error;
+
+ #[test]
+ fn test_client_closed_error() {
+ let err = Error::ClientClosed;
+ assert_eq!(format!("{}", err), "Client has been closed");
+ }
+
+ #[test]
+ fn test_api_error() {
+ let err = Error::Api {
+ status_code: 401,
+ message: "Invalid API key".to_string(),
+ };
+ assert!(format!("{}", err).contains("401"));
+ assert!(format!("{}", err).contains("Invalid API key"));
+ }
+
+ #[test]
+ fn test_invalid_argument_error() {
+ let err = Error::InvalidArgument("Bad parameter".to_string());
+ assert!(format!("{}", err).contains("Bad parameter"));
+ }
+}