Skip to content

Conversation

@nk-ag
Copy link
Contributor

@nk-ag nk-ag commented Sep 20, 2025

No description provided.

- Introduced a new TypeScript SDK for ExosphereHost, including core functionalities for state management, graph execution, and node handling.
- Added essential files: package.json, README.md, and TypeScript configuration.
- Implemented logging capabilities and error handling through custom signals.
- Included comprehensive tests to ensure functionality and reliability.
- Created a .gitignore file to exclude unnecessary files from version control.
…idation, and add utility functions

- Refactored endpoint construction in the Runtime class to use singular 'state' instead of 'states'.
- Introduced utility functions for schema validation: isZodObjectSchema and isZodStringSchema.
- Updated schema generation logic to use generateFlatSchema for inputs and outputs.
- Added Content-Type header to fetch requests in signals and state management.
- Included new utils.ts file for schema-related functions and updated tests accordingly.
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Sep 20, 2025

Note

Other AI code review bot(s) detected

CodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review.

📝 Walkthrough

Summary by CodeRabbit

  • New Features
    • Introduced a TypeScript SDK with a Runtime, BaseNode abstraction, models, utilities, and environment-driven logging.
    • Added signals for pruning and re-queuing workflows.
    • Provided a StateManager for graph upsert, trigger, retrieval, and validation.
    • Consolidated public exports and added JSON schema generation helpers.
  • Documentation
    • Comprehensive README with installation, quick start, advanced usage, and examples.
  • Tests
    • Extensive unit and integration tests with coverage configuration.
  • Chores
    • Package setup, build/test scripts, configs, and .gitignore.
    • CI to build, test, publish to npm, plus versioning and SBOM/coverage artifacts.

Walkthrough

Introduces a new TypeScript SDK for ExosphereHost, including core runtime, state manager, node base class, models, utilities, logger, signals, package config, tests, and CI workflow. Adds a single entrypoint export, comprehensive documentation, versioning scripts, and Vitest setup with extensive unit and integration-like tests.

Changes

Cohort / File(s) Summary of changes
Repository & Build/CI
typescript-sdk/.gitignore, typescript-sdk/package.json, typescript-sdk/tsconfig.json, typescript-sdk/vitest.config.ts, .github/workflows/publish-typescript-sdk.yml, typescript-sdk/scripts/version.js, typescript-sdk/scripts/version.bat
Adds ignore rules; initializes package, TypeScript config, Vitest config; introduces CI workflow for build/test/publish; adds versioning scripts (JS/Batch).
Public Entrypoints
typescript-sdk/exospherehost/index.ts, typescript-sdk/exospherehost/node/index.ts
Aggregates and re-exports SDK modules, exposing BaseNode and other public APIs from a single entry.
Core Runtime & Orchestration
typescript-sdk/exospherehost/runtime.ts, typescript-sdk/exospherehost/stateManager.ts, typescript-sdk/exospherehost/signals.ts
Implements Runtime (registration, enqueueing, worker execution, secrets, notifications, signal handling) and StateManager (trigger, getGraph, upsertGraph with polling). Adds Prune/ReQueueAfter signals.
Models, Types, Utils, Logger
typescript-sdk/exospherehost/models.ts, typescript-sdk/exospherehost/types.ts, typescript-sdk/exospherehost/utils.ts, typescript-sdk/exospherehost/logger.ts
Introduces Zod-based models (GraphNode, RetryPolicy, StoreConfig, Unites), public types, schema utilities, JSON Schema generator, and env-driven Logger.
Node Base Class
typescript-sdk/exospherehost/node/BaseNode.ts
Adds abstract BaseNode with static Zod schemas (Inputs/Outputs/Secrets), parsing/validation, and execution wrapper.
Documentation
typescript-sdk/README.md, typescript-sdk/tests/README.md
Adds main SDK README and tests README with setup and usage guidance.
Tests
typescript-sdk/tests/*.test.ts, typescript-sdk/tests/pending_*.test.ts
Adds comprehensive Vitest suites for BaseNode, Runtime, StateManager, models, signals, coverage, package init, edge cases, and pending integration scenarios.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor App as SDK Consumer
  participant RT as Runtime
  participant SM as StateManager API
  participant Node as BaseNode subclass
  participant Sec as Secrets Endpoint

  App->>RT: new Runtime(namespace, name, nodes, options)
  App->>RT: start()
  RT->>SM: POST /v{ver}/{ns}/register {nodes}
  SM-->>RT: 200 OK

  loop Enqueue cycle
    RT->>SM: POST /v{ver}/{ns}/{name}/enqueue {batchSize}
    SM-->>RT: 200 OK [{state...}]
    RT->>RT: Queue states
  end

  par Workers
    RT->>RT: Dequeue state
    RT->>Node: new Node()
    alt Node needs secrets
      RT->>Sec: GET /v{ver}/{ns}/secrets?node=...
      Sec-->>RT: 200 OK {secrets}
    end
    RT->>Node: _execute(inputs, secrets)
    alt Normal result
      RT->>SM: POST /.../executed {result}
      SM-->>RT: 200 OK
    else PruneSignal
      RT->>SM: POST /.../prune {data}
      SM-->>RT: 200 OK
    else ReQueueAfterSignal
      RT->>SM: POST /.../re-enqueue-after {enqueue_after}
      SM-->>RT: 200 OK
    else Error
      RT->>SM: POST /.../errored {error}
      SM-->>RT: 200 OK
    end
  end
Loading
sequenceDiagram
  autonumber
  actor Dev as SDK Consumer
  participant SM as StateManager API

  Dev->>SM: PUT /v{ver}/{ns}/graph/{graph} {nodes, secrets, retry, store_config}
  SM-->>Dev: 202 Accepted {validation: PENDING, id}
  loop Poll until terminal
    Dev->>SM: GET /v{ver}/{ns}/graph/{graph}
    SM-->>Dev: {validation: PENDING|VALID|INVALID}
  end
  alt VALID
    Dev->>SM: POST /v{ver}/{ns}/graph/{graph}/trigger {inputs, store, start_delay}
    SM-->>Dev: 200 OK {state_id}
  else INVALID or Timeout
    Dev-->>Dev: Throw error
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Possibly related PRs

Poem

A rabbit taps keys with a whiskered grin,
New nodes, new graphs—let the runs begin!
Signals hop, secrets burrow deep,
Workers queue while loggers peep.
With zod in paw and tests that sing,
To npm fields this bundle will spring. 🐇✨

Pre-merge checks and finishing touches

❌ Failed checks (2 warnings)
Check name Status Explanation Resolution
Description Check ⚠️ Warning No pull request description was provided by the author, leaving reviewers without a concise summary of intent, scope, test instructions, or any migration/compatibility notes which slows review. Please add a brief PR description that summarizes the purpose and high-level changes, lists key files/areas affected (package manifest, exospherehost modules, tests, CI workflow), notes required environment variables or migration steps, and gives quick guidance for how reviewers should validate the changes (build/test/publish steps).
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (1 passed)
Check name Status Explanation
Title Check ✅ Passed The title succinctly and accurately describes the main addition of a TypeScript SDK for Exosphere, matching the PR’s primary objective without unnecessary detail or noise.
✨ Finishing touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch TSsdk

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@nk-ag nk-ag changed the title T ssdk TypeScript SDK for Exosphere Sep 20, 2025
- Updated the getSecrets method to include 'Content-Type: application/json' in the fetch request headers, ensuring proper content handling for API interactions.
@nk-ag nk-ag marked this pull request as ready for review September 21, 2025 04:07
@nk-ag
Copy link
Contributor Author

nk-ag commented Sep 21, 2025

/gemini review

Copy link
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request introduces a comprehensive TypeScript SDK for Exosphere, mirroring the functionality of the existing Python SDK. The implementation is well-structured, covering the runtime, state manager, node definitions, and utility functions. The inclusion of a full test suite is excellent and ensures good coverage. I've identified several areas for improvement, primarily related to correctness in examples, code clarity, and dependency management. Key suggestions include correcting the usage examples in the README, refining Zod models for better maintainability, improving type safety in the BaseNode class, and adjusting the package dependencies. Overall, this is a solid foundation for the TypeScript SDK.

@coderabbitai coderabbitai bot added the enhancement New feature or request label Sep 21, 2025
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 73

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
typescript-sdk/tests/README.md (1)

78-86: Harden env var handling; add .env.example and per-test isolation.

Document .env usage, warn against committing secrets, and show Vitest helpers to isolate process.env.

Apply this diff:

 ## Environment Variables

 Tests set up required environment variables:
@@
 - `EXOSPHERE_API_KEY`

-These are automatically configured in test setup and cleaned up between tests.
+These are automatically configured in test setup and cleaned up between tests.
+
+Recommended:
+- Provide `typescript-sdk/.env.example` (no real secrets).
+- Load via `dotenv` in `vitest.setup.ts`, or inject in tests with helpers.
+- Snapshot/restore env per test:
+```ts
+const OLD_ENV = { ...process.env }
+beforeEach(() => { process.env = { ...OLD_ENV } })
+afterAll(() => { process.env = OLD_ENV })
+```
+- In CI, set secrets via secure variables; never commit real keys.

I can generate vitest.config.ts, vitest.setup.ts, and .env.example aligned with the SDK—say the word.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 17c194b and 07f961b.

⛔ Files ignored due to path filters (1)
  • typescript-sdk/package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (29)
  • typescript-sdk/.gitignore (1 hunks)
  • typescript-sdk/README.md (1 hunks)
  • typescript-sdk/exospherehost/index.ts (1 hunks)
  • typescript-sdk/exospherehost/logger.ts (1 hunks)
  • typescript-sdk/exospherehost/models.ts (1 hunks)
  • typescript-sdk/exospherehost/node/BaseNode.ts (1 hunks)
  • typescript-sdk/exospherehost/node/index.ts (1 hunks)
  • typescript-sdk/exospherehost/runtime.ts (1 hunks)
  • typescript-sdk/exospherehost/signals.ts (1 hunks)
  • typescript-sdk/exospherehost/stateManager.ts (1 hunks)
  • typescript-sdk/exospherehost/types.ts (1 hunks)
  • typescript-sdk/exospherehost/utils.ts (1 hunks)
  • typescript-sdk/package.json (1 hunks)
  • typescript-sdk/tests/README.md (1 hunks)
  • typescript-sdk/tests/test_base_node.test.ts (1 hunks)
  • typescript-sdk/tests/test_base_node_abstract.test.ts (1 hunks)
  • typescript-sdk/tests/test_base_node_comprehensive.test.ts (1 hunks)
  • typescript-sdk/tests/test_coverage_additions.test.ts (1 hunks)
  • typescript-sdk/tests/test_integration.test.ts (1 hunks)
  • typescript-sdk/tests/test_models_and_statemanager_new.test.ts (1 hunks)
  • typescript-sdk/tests/test_package_init.test.ts (1 hunks)
  • typescript-sdk/tests/test_runtime_comprehensive.test.ts (1 hunks)
  • typescript-sdk/tests/test_runtime_edge_cases.test.ts (1 hunks)
  • typescript-sdk/tests/test_runtime_validation.test.ts (1 hunks)
  • typescript-sdk/tests/test_signals_and_runtime_functions.test.ts (1 hunks)
  • typescript-sdk/tests/test_statemanager_comprehensive.test.ts (1 hunks)
  • typescript-sdk/tests/test_version.test.ts (1 hunks)
  • typescript-sdk/tsconfig.json (1 hunks)
  • typescript-sdk/vitest.config.ts (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (15)
typescript-sdk/tests/test_package_init.test.ts (1)
python-sdk/tests/test_package_init.py (1)
  • SampleNode (120-131)
typescript-sdk/tests/test_coverage_additions.test.ts (3)
typescript-sdk/exospherehost/stateManager.ts (1)
  • StateManager (10-134)
typescript-sdk/exospherehost/runtime.ts (1)
  • Runtime (58-353)
typescript-sdk/exospherehost/signals.ts (2)
  • PruneSignal (1-20)
  • ReQueueAfterSignal (22-44)
typescript-sdk/tests/test_runtime_edge_cases.test.ts (2)
python-sdk/tests/test_runtime_edge_cases.py (2)
  • MockTestNodeWithNonStringFields (24-36)
  • MockTestNodeWithoutSecrets (39-50)
typescript-sdk/exospherehost/runtime.ts (1)
  • Runtime (58-353)
typescript-sdk/exospherehost/models.ts (1)
python-sdk/exospherehost/models.py (1)
  • RetryStrategyEnum (78-89)
typescript-sdk/tests/test_signals_and_runtime_functions.test.ts (2)
typescript-sdk/exospherehost/signals.ts (2)
  • PruneSignal (1-20)
  • ReQueueAfterSignal (22-44)
typescript-sdk/exospherehost/runtime.ts (1)
  • Runtime (58-353)
typescript-sdk/tests/test_base_node.test.ts (1)
python-sdk/tests/test_base_node.py (1)
  • EchoNode (6-17)
typescript-sdk/tests/test_runtime_validation.test.ts (1)
typescript-sdk/exospherehost/runtime.ts (1)
  • Runtime (58-353)
typescript-sdk/exospherehost/runtime.ts (3)
typescript-sdk/exospherehost/logger.ts (2)
  • logger (70-70)
  • error (63-67)
typescript-sdk/exospherehost/utils.ts (3)
  • generateFlatSchema (27-50)
  • isZodObjectSchema (5-14)
  • isZodStringSchema (16-25)
typescript-sdk/exospherehost/signals.ts (2)
  • PruneSignal (1-20)
  • ReQueueAfterSignal (22-44)
typescript-sdk/tests/test_statemanager_comprehensive.test.ts (1)
typescript-sdk/exospherehost/stateManager.ts (1)
  • StateManager (10-134)
typescript-sdk/exospherehost/stateManager.ts (1)
typescript-sdk/exospherehost/models.ts (6)
  • GraphNodeModel (18-65)
  • GraphNodeModel (67-67)
  • RetryPolicyModel (83-89)
  • RetryPolicyModel (91-91)
  • StoreConfigModel (94-157)
  • StoreConfigModel (159-159)
typescript-sdk/tests/test_base_node_abstract.test.ts (1)
typescript-sdk/exospherehost/utils.ts (1)
  • isZodObjectSchema (5-14)
typescript-sdk/tests/test_integration.test.ts (3)
python-sdk/tests/test_integration.py (3)
  • IntegrationTestNode (42-59)
  • MultiOutputNode (62-74)
  • ErrorProneNode (77-90)
typescript-sdk/exospherehost/runtime.ts (1)
  • Runtime (58-353)
typescript-sdk/exospherehost/stateManager.ts (1)
  • StateManager (10-134)
typescript-sdk/tests/test_runtime_comprehensive.test.ts (1)
typescript-sdk/exospherehost/runtime.ts (1)
  • Runtime (58-353)
typescript-sdk/tests/test_models_and_statemanager_new.test.ts (1)
typescript-sdk/exospherehost/models.ts (6)
  • GraphNodeModel (18-65)
  • GraphNodeModel (67-67)
  • StoreConfigModel (94-157)
  • StoreConfigModel (159-159)
  • RetryPolicyModel (83-89)
  • RetryPolicyModel (91-91)
typescript-sdk/tests/test_base_node_comprehensive.test.ts (1)
typescript-sdk/exospherehost/utils.ts (2)
  • isZodObjectSchema (5-14)
  • isZodStringSchema (16-25)
🔇 Additional comments (21)
typescript-sdk/tests/README.md (2)

7-14: Confirmed — BaseNode test files exist and match the README. Found under typescript-sdk/tests: test_base_node.test.ts, test_base_node_abstract.test.ts, test_base_node_comprehensive.test.ts; each contains describe/it test blocks.


17-25: Incorrect — no casing mismatch found.
README lists test_statemanager_comprehensive.test.ts and that file exists at typescript-sdk/tests/test_statemanager_comprehensive.test.ts; imports reference ../exospherehost/stateManager.js (typescript-sdk/exospherehost/stateManager.ts) consistently.

Likely an incorrect or invalid review comment.

typescript-sdk/vitest.config.ts (3)

3-15: Baseline config looks good.

Globals + node env + V8 coverage with text/json/html reporters is a solid starting point.


1-3: No action required — no TS path aliases detected.
typescript-sdk/tsconfig.json contains no compilerOptions.paths, so adding vite-tsconfig-paths to vitest config is unnecessary.


8-13: Coverage config: enable all, add thresholds, and confirm glob scope

  • Verification inconclusive: the provided script showed 1 TS file matched exospherehost/**/*.ts and then failed to list non-included files (comm: /dev/fd/63). Confirm whether other SDK folders (models/, runtime/, stateManager/, node/, etc.) contain TS sources that should be covered; if so apply the diff below.
       coverage: {
         provider: 'v8',
         reporter: ['text', 'json', 'html'],
-        include: ['exospherehost/**/*.ts'],
-        exclude: ['exospherehost/**/*.d.ts', 'tests/**/*']
+        include: ['exospherehost/**/*.ts'],
+        exclude: [
+          'exospherehost/**/*.d.ts',
+          'tests/**/*',
+          '**/*.test.ts',
+          '**/*.spec.ts',
+          '**/vitest.config.*',
+          'dist/**'
+        ],
+        all: true,
+        thresholds: { statements: 80, branches: 70, functions: 80, lines: 80 },
+        reportsDirectory: 'coverage'
       }
typescript-sdk/exospherehost/node/index.ts (1)

1-1: LGTM

Re-export is correct and matches the public surface expectations.

typescript-sdk/tests/test_base_node.test.ts (1)

5-21: LGTM

Covers the core _execute path and state capture on the instance.

Also applies to: 23-34

typescript-sdk/package.json (1)

14-17: Make zod a peer dependency (avoid duplicate instances) and keep as devDep for building tests

Aligns with ecosystem practice and avoids schema identity mismatches for users defining nodes.

   "dependencies": {
-    "zod": "^3.23.8",
     "zod-to-json-schema": "^3.23.3"
   },
   "devDependencies": {
+    "zod": "^3.23.8",
     "@types/node": "^20.14.11",
     "@vitest/coverage-v8": "^1.6.0",
     "typescript": "^5.6.3",
     "vitest": "^1.6.0"
   },
   "peerDependencies": {
-    "@types/node": ">=20.0.0"
+    "@types/node": ">=20.0.0",
+    "zod": "^3.23.8"
   }

Also applies to: 18-23, 24-26

typescript-sdk/README.md (3)

23-36: Add required namespace in GraphNode examples

The schema requires namespace; example would fail parsing otherwise.

 const nodes: GraphNode[] = [
   {
     node_name: 'Start',
     identifier: 'start-node',
+    namespace: 'my-namespace',
     inputs: {},
     next_nodes: ['end-node']
   },
   {
     node_name: 'End',
     identifier: 'end-node',
+    namespace: 'my-namespace',
     inputs: {},
     next_nodes: []
   }
 ];

45-46: Pass only inputs to trigger

trigger expects Record<string,string> inputs, not the whole TriggerState.

-await sm.trigger('sample-graph', trigger);
+await sm.trigger('sample-graph', trigger.inputs);

54-57: Include the Secrets generic when extending BaseNode for clarity

Improves readability and avoids relying on defaults.

-class ExampleNode extends BaseNode<typeof ExampleNode.Inputs, typeof ExampleNode.Outputs> {
+class ExampleNode extends BaseNode<
+  typeof ExampleNode.Inputs,
+  typeof ExampleNode.Outputs,
+  typeof ExampleNode.Secrets
+> {
typescript-sdk/tests/test_runtime_comprehensive.test.ts (1)

80-83: Null output turns into [null] payload

Runtime wraps non-arrays in an array; null becomes [null]. Confirm contract: if “no outputs” is valid, normalize to [] before notifyExecuted; otherwise enforce schema and throw. Align test with the decided behavior.

typescript-sdk/tests/test_base_node_comprehensive.test.ts (2)

183-192: Strengthen null-output assertion.

Also assert Outputs schema isn’t parsed when result is null (post‑fix when BaseNode adds null to return type).

-    expect(result).toBeNull();
+    expect(result).toBeNull();
+    // ensure no array wrapping or schema parsing occurred
+    expect(Array.isArray(result)).toBe(false);

2-2: ESM import extension sanity-check — confirm .js resolution in tests

typesript-sdk/tests/test_base_node_comprehensive.test.ts imports ../exospherehost/node/BaseNode.js. I inspected tsconfig(s): one is module: ES2020 + moduleResolution: node; another is module: esnext + moduleResolution: bundler — neither uses "NodeNext". Confirm Vitest/Vite (the test runner) resolves .js imports at test time, or change the import (omit .js) or use a tsconfig with moduleResolution: "NodeNext" and appropriate ESM runtime config (package.json "type":"module" or test-runner ESM settings).

typescript-sdk/exospherehost/node/BaseNode.ts (2)

11-15: Constructor guard LGTM.


17-35: Include null in return types; remove unsafe as any cast.

Aligns with prior feedback; improves type safety for nodes legitimately returning no outputs.

-  async _execute(inputsRaw: unknown, secretsRaw: unknown): Promise<z.infer<O> | z.infer<O>[]> {
+  async _execute(inputsRaw: unknown, secretsRaw: unknown): Promise<z.infer<O> | z.infer<O>[] | null> {
@@
-    if (result === null) {
-      return null as any;
-    }
+    if (result === null) {
+      return null;
+    }
@@
-  abstract execute(): Promise<z.infer<O> | z.infer<O>[]>;
+  abstract execute(): Promise<z.infer<O> | z.infer<O>[] | null>;
typescript-sdk/exospherehost/models.ts (2)

19-23: Prefer trim().min(1) over transform+refine chain.

Cleaner and equivalent validation.

-  node_name: z.string()
-    .min(1, 'Node name cannot be empty')
-    .transform((val: string) => val.trim())
-    .refine((val: string) => val.length > 0, 'Node name cannot be empty')
+  node_name: z.string().trim().min(1, 'Node name cannot be empty')
@@
-  identifier: z.string()
-    .min(1, 'Node identifier cannot be empty')
-    .transform((val: string) => val.trim())
-    .refine((val: string) => val.length > 0, 'Node identifier cannot be empty')
+  identifier: z.string().trim().min(1, 'Node identifier cannot be empty')

Also applies to: 25-31


124-157: Remove redundant String(value) cast; prefer superRefine for errors.

-  default_values: z.record(z.string())
-    .transform((values: Record<string, string>) => {
+  default_values: z.record(z.string())
+    .transform((values: Record<string, string>) => {
       const errors: string[] = [];
       const keySet = new Set<string>();
       const normalizedDict: Record<string, string> = {};
@@
-        keySet.add(trimmedKey);
-        normalizedDict[trimmedKey] = String(value);
+        keySet.add(trimmedKey);
+        normalizedDict[trimmedKey] = value;
       }
@@
       return normalizedDict;
     })

Optional: replace the throw-based error collection with .superRefine prior to transform for better issue reporting.

typescript-sdk/exospherehost/runtime.ts (1)

151-154: Log response body, not [object Response], on register failure.

Same as prior feedback.

-    if (!res.ok) {
-      logger.error('Runtime', `Failed to register nodes: ${res}`);
-      throw new Error(`Failed to register nodes: ${res}`);
-    }
+    if (!res.ok) {
+      const errorText = await res.text();
+      logger.error('Runtime', `Failed to register nodes: ${errorText}`);
+      throw new Error(`Failed to register nodes: ${errorText}`);
+    }
typescript-sdk/exospherehost/stateManager.ts (2)

40-48: Use default parameters in signature instead of in-body undefined checks.

Cleaner and makes intent explicit.

-  async trigger(
-    graphName: string, 
-    inputs?: Record<string, string>, 
-    store?: Record<string, string>, 
-    startDelay: number = 0
-  ): Promise<unknown> {
-    if (inputs === undefined) inputs = {};
-    if (store === undefined) store = {};
+  async trigger(
+    graphName: string, 
+    inputs: Record<string, string> = {}, 
+    store: Record<string, string> = {}, 
+    startDelay: number = 0
+  ): Promise<unknown> {

92-103: Remove Pythonic model_dump branches; Zod outputs are plain objects.

Unnecessary checks add noise.

-    const body: any = { 
-      secrets, 
-      nodes: graphNodes.map(node => typeof node === 'object' && 'model_dump' in node ? (node as any).model_dump() : node)
-    };
+    const body: any = { 
+      secrets, 
+      nodes: graphNodes
+    };
@@
-    if (retryPolicy !== undefined) {
-      body.retry_policy = typeof retryPolicy === 'object' && 'model_dump' in retryPolicy ? (retryPolicy as any).model_dump() : retryPolicy;
-    }
-    if (storeConfig !== undefined) {
-      body.store_config = typeof storeConfig === 'object' && 'model_dump' in storeConfig ? (storeConfig as any).model_dump() : storeConfig;
-    }
+    if (retryPolicy !== undefined) body.retry_policy = retryPolicy;
+    if (storeConfig !== undefined) body.store_config = storeConfig;

Comment on lines +1 to +8
export * from './types.js';
export * from './models.js';
export * from './stateManager.js';
export * from './node/index.js';
export * from './runtime.js';
export * from './signals.js';
export * from './logger.js';
export * from './utils.js';
Copy link
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick

LGTM for public barrel.

Re-exports look coherent across types, models, runtime, signals, and utils.

Ensure package.json “exports” and “types” map to built ESM/CJS (e.g., dist/index.js + dist/index.d.ts) so consumers can import { Runtime } from '@exosphere/sdk' without pathing to files.

🤖 Prompt for AI Agents
In typescript-sdk/exospherehost/index.ts lines 1-8, the barrel exports are
correct but package.json currently may not point consumers to the built ESM/CJS
artifacts; update package.json so the package "exports" and type entry point map
to the compiled outputs (e.g., point "module" or appropriate export conditions
to dist/index.js for ESM, "main" or commonjs export to dist/index.cjs or
dist/index.js for CJS, and "types" to dist/index.d.ts), ensure the build step
emits those files (adjust tsconfig to emitDeclarationOnly or declaration: true
and set outDir to dist), and add proper export conditions in "exports" for "./"
and "./package.json" so consumers can import { Runtime } from '@exosphere/sdk'
without deep paths.

Comment on lines +27 to +32
describe('test_version_in_package_init', () => {
it('should expose version in package', () => {
// Test that version is properly exposed in package
expect(true).toBe(true); // Placeholder test
});
});
Copy link
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick

Rename suite and assert public API surface.

TS has no “package init”; check the public entry exports version.

Apply this diff:

-describe('test_version_in_package_init', () => {
-  it('should expose version in package', () => {
-    // Test that version is properly exposed in package
-    expect(true).toBe(true); // Placeholder test
-  });
-});
+describe('version: public API exposure', () => {
+  it('should expose `version` on the SDK surface', () => {
+    expect(Object.prototype.hasOwnProperty.call(sdk, 'version')).toBe(true);
+  });
+});
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
describe('test_version_in_package_init', () => {
it('should expose version in package', () => {
// Test that version is properly exposed in package
expect(true).toBe(true); // Placeholder test
});
});
describe('version: public API exposure', () => {
it('should expose `version` on the SDK surface', () => {
expect(Object.prototype.hasOwnProperty.call(sdk, 'version')).toBe(true);
});
});
🤖 Prompt for AI Agents
In typescript-sdk/tests/test_version.test.ts around lines 27-32, the test suite
name and assertion are placeholders; rename the suite to something like "version
export" and replace the placeholder assertion with a concrete check of the
public API: import the package.json version (from the repo root) and import the
SDK's public export (the package entry point) that should expose "version", then
assert that the exported version exists, is a string, and equals the
package.json version (e.g. expect(exported.version).toBeDefined(); expect(typeof
exported.version).toBe('string');
expect(exported.version).toBe(packageJson.version)).

Comment on lines +3 to +6
"target": "ES2020",
"module": "ES2020",
"moduleResolution": "node",
"declaration": true,
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Adopt ESM-friendly resolution to match .js extension imports in TS

Current "moduleResolution":"node" + ESM can mis-typecheck imports like './Foo.js' to TS sources. Prefer Bundler (for Vitest/Vite) or NodeNext.

   "compilerOptions": {
-    "target": "ES2020",
-    "module": "ES2020",
-    "moduleResolution": "node",
+    "target": "ES2022",
+    "module": "ES2022",
+    "moduleResolution": "Bundler",
+    "verbatimModuleSyntax": true,
     "declaration": true,
     "rootDir": "exospherehost",
     "outDir": "dist",
     "strict": true,
     "esModuleInterop": true,
     "forceConsistentCasingInFileNames": true,
-    "skipLibCheck": true
+    "skipLibCheck": true,
+    "sourceMap": true,
+    "declarationMap": true
   },

Also applies to: 8-12

🤖 Prompt for AI Agents
In typescript-sdk/tsconfig.json around lines 3-6 (and similarly lines 8-12), the
current "moduleResolution": "node" can mis-typecheck ESM-style imports that
include .js extensions; change moduleResolution to an ESM-friendly option such
as "bundler" (recommended for Vite/Vitest) or "nodenext" to correctly resolve
'./Foo.js' imports, and update the same setting in the other block mentioned
(lines 8-12) so both compiler configurations use the ESM-aware resolver.

Comment on lines +14 to +15
"include": ["exospherehost/**/*"]
}
Copy link
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick

Consider including tests in tsconfig for type-check only

If you want tests type-checked (no emit), add a separate tsconfig or a script using tsc --noEmit over tests too. Keeps runtime tsconfig clean.

🤖 Prompt for AI Agents
In typescript-sdk/tsconfig.json around lines 14-15 the project tsconfig
currently includes only "exospherehost/**/*" and the reviewer suggests
type-checking tests separately; create a new tsconfig for tests (e.g.,
tsconfig.tests.json) that extends ./tsconfig.json, set "noEmit": true and
include your test folders (e.g., "tests/**/*" or "src/**/*.spec.ts"), and add an
npm script (e.g., "typecheck:tests" running tsc -p tsconfig.tests.json or use
tsc --noEmit against test files) so tests are type-checked without changing the
runtime tsconfig.

Comment on lines +3 to +4
export default defineConfig({
test: {
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Set an explicit Vitest project root (monorepo safety).

When run from the repo root, Vitest may resolve globs relative to CWD, skewing test/coverage paths. Pin root to the config’s directory.

Apply this diff:

+import { fileURLToPath } from 'node:url';
 import { defineConfig } from 'vitest/config';

 export default defineConfig({
+  root: fileURLToPath(new URL('.', import.meta.url)),
   test: {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export default defineConfig({
test: {
import { fileURLToPath } from 'node:url';
import { defineConfig } from 'vitest/config';
export default defineConfig({
root: fileURLToPath(new URL('.', import.meta.url)),
test: {
🤖 Prompt for AI Agents
In typescript-sdk/vitest.config.ts around lines 3-4, the Vitest config needs an
explicit root pinned to the config file’s directory so globs and coverage
resolve correctly; add a root property set to the config directory (e.g., root:
path.resolve(__dirname)) and ensure you import path (or, if using ESM, compute
__dirname via fileURLToPath/dirname) so Vitest always resolves relative to the
config file rather than the repo CWD.

test: {
globals: true,
environment: 'node',
include: ['tests/**/*.test.ts'],
Copy link
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick

*Widen test globs to catch .spec.ts as well.

Prevents missed tests if contributors use the common .spec.ts suffix.

Apply this diff:

-    include: ['tests/**/*.test.ts'],
+    include: ['tests/**/*.{test,spec}.ts'],
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
include: ['tests/**/*.test.ts'],
include: ['tests/**/*.{test,spec}.ts'],
🤖 Prompt for AI Agents
In typescript-sdk/vitest.config.ts around line 7, the include glob currently
only matches tests/**/*.test.ts and will miss files using the common .spec.ts
suffix; update the include entry to also match .spec.ts (e.g., change the
pattern to tests/**/*.test.ts and tests/**/*.spec.ts or use a combined glob like
tests/**/*.{test,spec}.ts) so both .test.ts and .spec.ts files are discovered by
Vitest.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Review continued from previous batch...

Comment on lines +8 to +23
export class Logger {
private static instance: Logger;
private level: LogLevel;
private isDisabled: boolean;

private constructor() {
this.level = this.getLogLevelFromEnv();
this.isDisabled = process.env.EXOSPHERE_DISABLE_DEFAULT_LOGGING === 'true';
}

public static getInstance(): Logger {
if (!Logger.instance) {
Logger.instance = new Logger();
}
return Logger.instance;
}
Copy link
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick

Expose runtime controls for tests and dynamic configs

Provide setLevel/setDisabled to avoid relying on process.env at import-time only.

   private level: LogLevel;
   private isDisabled: boolean;
@@
   }
 
+  public setLevel(level: LogLevel) { this.level = level; }
+  public setDisabled(disabled: boolean) { this.isDisabled = disabled; }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export class Logger {
private static instance: Logger;
private level: LogLevel;
private isDisabled: boolean;
private constructor() {
this.level = this.getLogLevelFromEnv();
this.isDisabled = process.env.EXOSPHERE_DISABLE_DEFAULT_LOGGING === 'true';
}
public static getInstance(): Logger {
if (!Logger.instance) {
Logger.instance = new Logger();
}
return Logger.instance;
}
export class Logger {
private static instance: Logger;
private level: LogLevel;
private isDisabled: boolean;
private constructor() {
this.level = this.getLogLevelFromEnv();
this.isDisabled = process.env.EXOSPHERE_DISABLE_DEFAULT_LOGGING === 'true';
}
public setLevel(level: LogLevel) { this.level = level; }
public setDisabled(disabled: boolean) { this.isDisabled = disabled; }
public static getInstance(): Logger {
if (!Logger.instance) {
Logger.instance = new Logger();
}
return Logger.instance;
}
}
🤖 Prompt for AI Agents
In typescript-sdk/exospherehost/logger.ts around lines 8 to 23, the Logger
currently reads log level and disabled flag only at construction from
process.env, preventing tests and runtime code from adjusting them; add public
setLevel(level: LogLevel) and setDisabled(disabled: boolean) methods that update
the instance's level and isDisabled fields at runtime, keep the constructor
behavior of initializing from env, and ensure getInstance continues to return
the single Logger so callers (including tests) can call setLevel/setDisabled to
alter behavior without modifying environment variables.

Comment on lines +13 to +16
private constructor() {
this.level = this.getLogLevelFromEnv();
this.isDisabled = process.env.EXOSPHERE_DISABLE_DEFAULT_LOGGING === 'true';
}
Copy link
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick

Boolean env parsing is too strict

Support common true/false forms (true/1/yes/on).

Apply this diff:

-    this.isDisabled = process.env.EXOSPHERE_DISABLE_DEFAULT_LOGGING === 'true';
+    const v = (process.env.EXOSPHERE_DISABLE_DEFAULT_LOGGING || '').toString().toLowerCase();
+    this.isDisabled = ['true','1','yes','on'].includes(v);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
private constructor() {
this.level = this.getLogLevelFromEnv();
this.isDisabled = process.env.EXOSPHERE_DISABLE_DEFAULT_LOGGING === 'true';
}
private constructor() {
this.level = this.getLogLevelFromEnv();
const v = (process.env.EXOSPHERE_DISABLE_DEFAULT_LOGGING || '').toString().toLowerCase();
this.isDisabled = ['true','1','yes','on'].includes(v);
}

Comment on lines +40 to +43
private formatMessage(level: string, name: string, message: string): string {
const timestamp = new Date().toISOString().replace('T', ' ').substring(0, 19);
return `${timestamp} | ${level} | ${name} | ${message}`;
}
Copy link
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick

Preserve milliseconds in timestamps

Current substring drops ms. Keep full ISO timestamp for better tracing.

-    const timestamp = new Date().toISOString().replace('T', ' ').substring(0, 19);
-    return `${timestamp} | ${level} | ${name} | ${message}`;
+    const timestamp = new Date().toISOString(); // includes milliseconds, UTC
+    return `${timestamp} | ${level} | ${name} | ${message}`;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
private formatMessage(level: string, name: string, message: string): string {
const timestamp = new Date().toISOString().replace('T', ' ').substring(0, 19);
return `${timestamp} | ${level} | ${name} | ${message}`;
}
private formatMessage(level: string, name: string, message: string): string {
const timestamp = new Date().toISOString(); // includes milliseconds, UTC
return `${timestamp} | ${level} | ${name} | ${message}`;
}
🤖 Prompt for AI Agents
In typescript-sdk/exospherehost/logger.ts around lines 40 to 43, the timestamp
generation currently truncates milliseconds via substring(0,19); remove the
substring call so the full ISO timestamp (including milliseconds) is
preserved—e.g., use new Date().toISOString().replace('T', ' ') (or otherwise
avoid slicing) and return the formatted string as before.

Comment on lines +45 to +67
public debug(name: string, message: string): void {
if (this.shouldLog(LogLevel.DEBUG)) {
console.debug(this.formatMessage('DEBUG', name, message));
}
}

public info(name: string, message: string): void {
if (this.shouldLog(LogLevel.INFO)) {
console.info(this.formatMessage('INFO', name, message));
}
}

public warn(name: string, message: string): void {
if (this.shouldLog(LogLevel.WARN)) {
console.warn(this.formatMessage('WARN', name, message));
}
}

public error(name: string, message: string): void {
if (this.shouldLog(LogLevel.ERROR)) {
console.error(this.formatMessage('ERROR', name, message));
}
}
Copy link
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick

Allow non-string payloads and Error objects

Broaden message type and include stack traces automatically.

Apply this diff:

-  public debug(name: string, message: string): void {
+  private toText(payload: unknown): string {
+    if (payload instanceof Error) return payload.stack || payload.message;
+    if (typeof payload === 'string') return payload;
+    try { return JSON.stringify(payload); } catch { return String(payload); }
+  }
+
+  public debug(name: string, message: unknown): void {
     if (this.shouldLog(LogLevel.DEBUG)) {
-      console.debug(this.formatMessage('DEBUG', name, message));
+      console.debug(this.formatMessage('DEBUG', name, this.toText(message)));
     }
   }
 
-  public info(name: string, message: string): void {
+  public info(name: string, message: unknown): void {
     if (this.shouldLog(LogLevel.INFO)) {
-      console.info(this.formatMessage('INFO', name, message));
+      console.info(this.formatMessage('INFO', name, this.toText(message)));
     }
   }
 
-  public warn(name: string, message: string): void {
+  public warn(name: string, message: unknown): void {
     if (this.shouldLog(LogLevel.WARN)) {
-      console.warn(this.formatMessage('WARN', name, message));
+      console.warn(this.formatMessage('WARN', name, this.toText(message)));
     }
   }
 
-  public error(name: string, message: string): void {
+  public error(name: string, message: unknown): void {
     if (this.shouldLog(LogLevel.ERROR)) {
-      console.error(this.formatMessage('ERROR', name, message));
+      console.error(this.formatMessage('ERROR', name, this.toText(message)));
     }
   }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
public debug(name: string, message: string): void {
if (this.shouldLog(LogLevel.DEBUG)) {
console.debug(this.formatMessage('DEBUG', name, message));
}
}
public info(name: string, message: string): void {
if (this.shouldLog(LogLevel.INFO)) {
console.info(this.formatMessage('INFO', name, message));
}
}
public warn(name: string, message: string): void {
if (this.shouldLog(LogLevel.WARN)) {
console.warn(this.formatMessage('WARN', name, message));
}
}
public error(name: string, message: string): void {
if (this.shouldLog(LogLevel.ERROR)) {
console.error(this.formatMessage('ERROR', name, message));
}
}
private toText(payload: unknown): string {
if (payload instanceof Error) return payload.stack || payload.message;
if (typeof payload === 'string') return payload;
try { return JSON.stringify(payload); } catch { return String(payload); }
}
public debug(name: string, message: unknown): void {
if (this.shouldLog(LogLevel.DEBUG)) {
console.debug(this.formatMessage('DEBUG', name, this.toText(message)));
}
}
public info(name: string, message: unknown): void {
if (this.shouldLog(LogLevel.INFO)) {
console.info(this.formatMessage('INFO', name, this.toText(message)));
}
}
public warn(name: string, message: unknown): void {
if (this.shouldLog(LogLevel.WARN)) {
console.warn(this.formatMessage('WARN', name, this.toText(message)));
}
}
public error(name: string, message: unknown): void {
if (this.shouldLog(LogLevel.ERROR)) {
console.error(this.formatMessage('ERROR', name, this.toText(message)));
}
}
🤖 Prompt for AI Agents
In typescript-sdk/exospherehost/logger.ts around lines 45 to 67, the log methods
only accept string messages and therefore drop non-string payloads and Error
stack traces; change the public debug/info/warn/error signatures to accept a
broader type (e.g., unknown | Error), then normalize the payload before passing
to formatMessage: if the value is an Error, use its stack (fallback to message),
if it's a non-string object, JSON.stringify it (with safe/circular handling or a
replacer), and otherwise convert toString(); update formatMessage to accept a
string already-normalized message and ensure the Error stack gets logged
automatically.

Comment on lines +33 to +55
.transform((nodes: string[]) => nodes.map((node: string) => node.trim()))
.refine((nodes: string[]) => {
const errors: string[] = [];
const identifiers = new Set<string>();

for (const node of nodes) {
if (node === '') {
errors.push('Next node identifier cannot be empty');
continue;
}
if (identifiers.has(node)) {
errors.push(`Next node identifier ${node} is not unique`);
continue;
}
identifiers.add(node);
}

if (errors.length > 0) {
throw new Error(errors.join('\n'));
}
return nodes;
})
.optional()
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Use superRefine for per-item errors; avoid throwing inside refine.

Throwing breaks Zod’s issue reporting. Return issues via context instead.

-  next_nodes: z.array(z.string())
-    .transform((nodes: string[]) => nodes.map((node: string) => node.trim()))
-    .refine((nodes: string[]) => {
-      const errors: string[] = [];
-      const identifiers = new Set<string>();
-      
-      for (const node of nodes) {
-        if (node === '') {
-          errors.push('Next node identifier cannot be empty');
-          continue;
-        }
-        if (identifiers.has(node)) {
-          errors.push(`Next node identifier ${node} is not unique`);
-          continue;
-        }
-        identifiers.add(node);
-      }
-      
-      if (errors.length > 0) {
-        throw new Error(errors.join('\n'));
-      }
-      return nodes;
-    })
+  next_nodes: z.array(z.string())
+    .transform((nodes: string[]) => nodes.map((node: string) => node.trim()))
+    .superRefine((nodes, ctx) => {
+      const seen = new Set<string>();
+      nodes.forEach((node, idx) => {
+        if (node === '') {
+          ctx.addIssue({ code: z.ZodIssueCode.custom, message: 'Next node identifier cannot be empty', path: [idx] });
+        } else if (seen.has(node)) {
+          ctx.addIssue({ code: z.ZodIssueCode.custom, message: `Next node identifier '${node}' is not unique`, path: [idx] });
+        } else {
+          seen.add(node);
+        }
+      });
+    })

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In typescript-sdk/exospherehost/models.ts around lines 33 to 55, the current
.refine implementation throws an Error for per-item validation which breaks
Zod's issue reporting; replace .refine with .superRefine((nodes, ctx) => { ...
}) and inside iterate trimmed nodes, for each empty or duplicate node call
ctx.addIssue(...) with appropriate message and path (index) instead of pushing
to an errors array or throwing, then return nothing (Zod collects issues); keep
the .transform and .optional chaining intact.

Comment on lines +41 to +43
const count = parseInt(this.inputs.count);
return Array.from({ length: count }, (_, i) => ({ numbers: i.toString() }));
}
Copy link
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick

parseInt without radix

Add radix to avoid locale-dependent parsing.

Apply this diff:

-    const count = parseInt(this.inputs.count);
+    const count = parseInt(this.inputs.count, 10);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const count = parseInt(this.inputs.count);
return Array.from({ length: count }, (_, i) => ({ numbers: i.toString() }));
}
const count = parseInt(this.inputs.count, 10);
return Array.from({ length: count }, (_, i) => ({ numbers: i.toString() }));
}
🤖 Prompt for AI Agents
In typescript-sdk/tests/test_runtime_comprehensive.test.ts around lines 41 to
43, parseInt is called without a radix which can lead to locale-dependent
parsing; update the call to include an explicit radix (e.g.,
parseInt(this.inputs.count, 10)) so the input is always parsed as base 10 and
ensure any other parseInt uses in the file also include the radix.

Comment on lines +201 to +204
const runtime = new Runtime('test_namespace', 'test_runtime', [MockTestNode]);

await expect((runtime as any).register()).rejects.toThrow('Failed to register nodes');
});
Copy link
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick

Assertion on error message is brittle

Use regex to match the prefix so runtime can include details (status/text).

Apply this diff:

-    await expect((runtime as any).register()).rejects.toThrow('Failed to register nodes');
+    await expect((runtime as any).register()).rejects.toThrow(/Failed to register nodes/);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const runtime = new Runtime('test_namespace', 'test_runtime', [MockTestNode]);
await expect((runtime as any).register()).rejects.toThrow('Failed to register nodes');
});
const runtime = new Runtime('test_namespace', 'test_runtime', [MockTestNode]);
await expect((runtime as any).register()).rejects.toThrow(/Failed to register nodes/);
});
🤖 Prompt for AI Agents
In typescript-sdk/tests/test_runtime_comprehensive.test.ts around lines 201 to
204, the test currently asserts the exact error message 'Failed to register
nodes' which is brittle; update the expectation to use a regex that matches the
error prefix (e.g., /^Failed to register nodes/) so the test will accept
additional details like status or text appended to the message; change the
assertion to expect(...).rejects.toThrow(/regex/) accordingly.

Comment on lines +268 to +278
const workerPromise = (runtime as any).worker(1);

// Wait a bit for processing
await new Promise(resolve => setTimeout(resolve, 100));

// Verify secrets were fetched
expect((runtime as any).getSecrets).toHaveBeenCalledWith('test_state_1');

// Verify execution was notified
expect((runtime as any).notifyExecuted).toHaveBeenCalled();
});
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Infinite worker loop in tests can leak async tasks

worker() is an endless loop. Prefer driving a single iteration via a helper or mock stateQueue.get() to resolve once and then return a never-resolving promise, and ensure the test runner doesn’t await the worker. Alternatively, introduce an abort signal into Runtime for testability.

Example change (within the test) to constrain to one iteration:

-    const workerPromise = (runtime as any).worker(1);
+    const originalGet = (runtime as any).stateQueue.get.bind((runtime as any).stateQueue);
+    const getSpy = vi.spyOn((runtime as any).stateQueue, 'get')
+      .mockImplementationOnce(originalGet)
+      .mockImplementation(() => new Promise(() => {})); // park forever after one item
+    (runtime as any).worker(1); // don't await

Also applies to: 296-301, 319-324, 342-347

Comment on lines +287 to +288
vi.spyOn(runtime as any, 'register').mockResolvedValue({ status: 'registered' });

Copy link
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick

Unused spies

Spying on register() here has no effect. Remove to reduce noise.

-    vi.spyOn(runtime as any, 'register').mockResolvedValue({ status: 'registered' });

Also applies to: 310-311, 333-334

🤖 Prompt for AI Agents
In typescript-sdk/tests/test_runtime_comprehensive.test.ts around lines 287-288
(and similarly at 310-311 and 333-334), the vi.spyOn(runtime as any,
'register').mockResolvedValue(...) calls are unused and add noise; remove those
spy/mock lines from those locations so the tests no longer create unnecessary
spies, ensuring any required behavior is covered by the existing test setup or
by targeted spies elsewhere.

Comment on lines +126 to +147
it('should raise error for duplicate node names', () => {
class GoodNode1 extends BaseNode {
static Inputs = z.object({ name: z.string() });
static Outputs = z.object({ message: z.string() });
static Secrets = z.object({ api_key: z.string() });
async execute() { return { message: 'ok' }; }
}

class GoodNode2 extends BaseNode {
static Inputs = z.object({ name: z.string() });
static Outputs = z.object({ message: z.string() });
static Secrets = z.object({ api_key: z.string() });
async execute() { return { message: 'ok' }; }
}

// Use the same name for both classes
Object.defineProperty(GoodNode2, 'name', { value: 'GoodNode1' });

expect(() => {
new Runtime('ns', 'rt', [GoodNode1, GoodNode2]);
}).toThrow();
});
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Fix duplicate-class-name test — you can’t reliably overwrite name on classes.

Overwriting Function.prototype.name is non-writable in many runtimes; this can throw. Use named class expressions with the same internal name instead.

-    class GoodNode1 extends BaseNode {
+    const GoodNode1 = class GoodNodeDup extends BaseNode {
       static Inputs = z.object({ name: z.string() });
       static Outputs = z.object({ message: z.string() });
       static Secrets = z.object({ api_key: z.string() });
       async execute() { return { message: 'ok' }; }
-    }
+    };
     
-    class GoodNode2 extends BaseNode {
+    const GoodNode2 = class GoodNodeDup extends BaseNode {
       static Inputs = z.object({ name: z.string() });
       static Outputs = z.object({ message: z.string() });
       static Secrets = z.object({ api_key: z.string() });
       async execute() { return { message: 'ok' }; }
-    }
-    
-    // Use the same name for both classes
-    Object.defineProperty(GoodNode2, 'name', { value: 'GoodNode1' });
+    };
     
     expect(() => {
       new Runtime('ns', 'rt', [GoodNode1, GoodNode2]);
     }).toThrow();
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
it('should raise error for duplicate node names', () => {
class GoodNode1 extends BaseNode {
static Inputs = z.object({ name: z.string() });
static Outputs = z.object({ message: z.string() });
static Secrets = z.object({ api_key: z.string() });
async execute() { return { message: 'ok' }; }
}
class GoodNode2 extends BaseNode {
static Inputs = z.object({ name: z.string() });
static Outputs = z.object({ message: z.string() });
static Secrets = z.object({ api_key: z.string() });
async execute() { return { message: 'ok' }; }
}
// Use the same name for both classes
Object.defineProperty(GoodNode2, 'name', { value: 'GoodNode1' });
expect(() => {
new Runtime('ns', 'rt', [GoodNode1, GoodNode2]);
}).toThrow();
});
it('should raise error for duplicate node names', () => {
const GoodNode1 = class GoodNodeDup extends BaseNode {
static Inputs = z.object({ name: z.string() });
static Outputs = z.object({ message: z.string() });
static Secrets = z.object({ api_key: z.string() });
async execute() { return { message: 'ok' }; }
};
const GoodNode2 = class GoodNodeDup extends BaseNode {
static Inputs = z.object({ name: z.string() });
static Outputs = z.object({ message: z.string() });
static Secrets = z.object({ api_key: z.string() });
async execute() { return { message: 'ok' }; }
};
expect(() => {
new Runtime('ns', 'rt', [GoodNode1, GoodNode2]);
}).toThrow();
});
🤖 Prompt for AI Agents
In typescript-sdk/tests/test_runtime_validation.test.ts around lines 126-147,
the test attempts to overwrite a class's name via Object.defineProperty which is
non-writable in many runtimes and can throw; replace that hack by creating
GoodNode2 as a named class expression that uses the same internal class name as
GoodNode1 (e.g., const GoodNode2 = class GoodNode1 extends BaseNode { ... }) so
both classes share the same internal name without touching
Function.prototype.name, keeping the exported variable name GoodNode2 and
leaving the rest of the test unchanged.

…larity and functionality

- Added Zod dependency to both devDependencies and peerDependencies in package.json for schema validation.
- Expanded README.md to provide a comprehensive overview of the ExosphereHost TypeScript SDK, including installation instructions, key features, and usage examples.
- Improved error handling in the Runtime class by logging detailed error messages when node registration fails.
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 7

📜 Review details

Configuration used: CodeRabbit UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 07f961b and f559144.

📒 Files selected for processing (3)
  • typescript-sdk/README.md (1 hunks)
  • typescript-sdk/exospherehost/runtime.ts (1 hunks)
  • typescript-sdk/package.json (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
typescript-sdk/exospherehost/runtime.ts (3)
typescript-sdk/exospherehost/logger.ts (2)
  • logger (70-70)
  • error (63-67)
typescript-sdk/exospherehost/utils.ts (3)
  • generateFlatSchema (27-50)
  • isZodObjectSchema (5-14)
  • isZodStringSchema (16-25)
typescript-sdk/exospherehost/signals.ts (2)
  • PruneSignal (1-20)
  • ReQueueAfterSignal (22-44)
🪛 markdownlint-cli2 (0.18.1)
typescript-sdk/README.md

391-391: Trailing spaces
Expected: 0 or 2; Actual: 1

(MD009, no-trailing-spaces)


509-509: Files should end with a single newline character

(MD047, single-trailing-newline)

🔇 Additional comments (14)
typescript-sdk/package.json (3)

6-7: Ensure dist entry points exist before publish (build via lifecycle).

Add “prepare”/“prepublishOnly” (above) so dist/index.{js,d.ts} exists for consumers.

#!/bin/bash
# Verify manifest and artifacts (no build/run).
set -euo pipefail
sed -n '1,160p' typescript-sdk/package.json
echo "---- dist contents ----"
if [ -d typescript-sdk/dist ]; then ls -la typescript-sdk/dist; else echo "MISSING: typescript-sdk/dist"; fi
echo "---- aggregator ----"
if [ -f typescript-sdk/exospherehost/index.ts ]; then sed -n '1,160p' typescript-sdk/exospherehost/index.ts; else echo "MISSING: exospherehost/index.ts"; fi

1-27: Harden package for publish/consumption: exports map, files, license, engines, sideEffects, prepare/prepublish, optional @types/node peer, typecheck script

Prevents deep imports, ensures build artifacts ship, and documents runtime requirements. Also marks @types/node peer as optional.

 {
   "name": "exospherehost",
   "version": "0.1.0",
   "description": "Official TypeScript SDK for ExosphereHost",
   "type": "module",
   "main": "dist/index.js",
   "types": "dist/index.d.ts",
+  "exports": {
+    ".": {
+      "types": "./dist/index.d.ts",
+      "import": "./dist/index.js"
+    }
+  },
+  "files": [
+    "dist",
+    "README.md",
+    "LICENSE"
+  ],
+  "license": "MIT",
+  "sideEffects": false,
+  "engines": {
+    "node": ">=18.17"
+  },
   "scripts": {
     "build": "tsc",
+    "typecheck": "tsc -p tsconfig.json --noEmit",
+    "prepare": "npm run build",
+    "prepublishOnly": "npm run typecheck && npm run build && npm run test:run",
     "test": "vitest",
     "test:run": "vitest run",
     "test:coverage": "vitest run --coverage"
   },
   "dependencies": {
     "zod-to-json-schema": "^3.23.3"
   },
   "devDependencies": {
     "@types/node": "^20.14.11",
     "@vitest/coverage-v8": "^1.6.0",
     "typescript": "^5.6.3",
     "vitest": "^1.6.0",
     "zod": "^3.23.8"
   },
   "peerDependencies": {
     "@types/node": ">=20.0.0",
     "zod": "^3.23.8"
+  },
+  "peerDependenciesMeta": {
+    "@types/node": {
+      "optional": true
+    }
   }
 }

2-2: Package name "exospherehost" appears available on npm (verified)

npm registry returned HTTP 404 for https://registry.npmjs.org/exospherehost, indicating the name is currently unregistered. File: typescript-sdk/package.json contains "name": "exospherehost".

typescript-sdk/exospherehost/runtime.ts (8)

151-155: Good fix: log response body on registration failures

Using res.text() improves observability.


51-56: Stabilize node identifier against bundler name-mangling

Prefer explicit static NodeName when present; fall back to .name.

-type NodeCtor = (new () => BaseNode<any, any, any>) & {
+type NodeCtor = (new () => BaseNode<any, any, any>) & {
   Inputs: ZodObject<any>;
   Outputs: ZodObject<any>;
   Secrets: ZodObject<any>;
-  name: string;
+  name: string;
+  NodeName?: string;
 };

69-89: Build mapping with canonical names

Use NodeName??name for nodeMapping/nodeNames.

   ) {
@@
-    this.stateQueue = new AsyncQueue<StateItem>(2 * this.batchSize);
-    this.nodeMapping = Object.fromEntries(nodes.map(n => [n.name, n]));
-    this.nodeNames = nodes.map(n => n.name);
+    this.stateQueue = new AsyncQueue<StateItem>(2 * this.batchSize);
+    const getName = (n: NodeCtor) => n.NodeName ?? n.name;
+    this.nodeMapping = Object.fromEntries(nodes.map(n => [getName(n), n]));
+    this.nodeNames = nodes.map(getName);
@@
     logger.debug('Runtime', `Initialized runtime with namespace: ${this.namespace}, name: ${this.name}, nodes: ${this.nodeNames.join(', ')}`);

320-325: Don’t send [null]; treat null/undefined as no outputs

Prevents API receiving [null].

-        const outArray = Array.isArray(outputs) ? outputs : [outputs];
+        const outArray =
+          outputs == null ? [] : (Array.isArray(outputs) ? outputs : [outputs]);
         await this.notifyExecuted(state.state_id, outArray);

345-352: “Started workers” is unreachable; move log before await

Promise.all never resolves under normal operation.

-    const poller = this.enqueue();
-    const workers = Array.from({ length: this.workers }, (_, idx) => this.worker(idx));
-    await Promise.all([poller, ...workers]);
-    logger.info('Runtime', `Started workers: ${this.workers}`);
+    const poller = this.enqueue();
+    const workers = Array.from({ length: this.workers }, (_, idx) => this.worker(idx));
+    logger.info('Runtime', `Starting workers: ${this.workers}`);
+    await Promise.all([poller, ...workers]); // runs until shutdown

128-141: Register using canonical names (NodeName??name)

Ensures registration/enqueue/dispatch use a consistent identifier.

-    const nodeNames = this.nodes.map(node => `${this.namespace}/${node.name}`);
+    const nodeNames = this.nodes.map(node => `${this.namespace}/${(node.NodeName ?? node.name)}`);
@@
-      nodes: this.nodes.map(node => ({
-        name: node.name,
+      nodes: this.nodes.map(node => ({
+        name: node.NodeName ?? node.name,
         inputs_schema: generateFlatSchema(node.Inputs, 'Inputs'),
         outputs_schema: generateFlatSchema(node.Outputs, 'Outputs'),
         secrets: Object.keys((node.Secrets as ZodObject<any>).shape)
       }))

23-49: AsyncQueue capacity is unused; enforce backpressure

Without enforcing capacity, enqueue can grow unbounded under slow consumers.

 class AsyncQueue<T> {
   private items: T[] = [];
-  private resolvers: ((value: T) => void)[] = [];
+  private resolvers: ((value: T) => void)[] = [];
+  private pendingPuts: (() => void)[] = [];
   constructor(private capacity: number) {}
@@
-  async put(item: T) {
-    if (this.resolvers.length) {
+  async put(item: T) {
+    if (this.items.length >= this.capacity && this.resolvers.length === 0) {
+      await new Promise<void>(resolve => this.pendingPuts.push(resolve));
+    }
+    if (this.resolvers.length) {
       const resolve = this.resolvers.shift()!;
       resolve(item);
     } else {
       this.items.push(item);
     }
   }
@@
   async get(): Promise<T> {
     if (this.items.length) {
-      return this.items.shift()!;
+      const it = this.items.shift()!;
+      const release = this.pendingPuts.shift();
+      if (release) release();
+      return it;
     }
     return new Promise<T>(resolve => {
       this.resolvers.push(resolve);
     });
   }
 }

246-290: Duplicate-name check should use canonical name

Avoid false negatives if NodeName differs.

-    const names = this.nodes.map(n => n.name);
+    const names = this.nodes.map(n => n.NodeName ?? n.name);
typescript-sdk/README.md (3)

21-23: Document Node.js requirement and env-var defaults in Installation

Make runtime expectations explicit (Node 18+ for global fetch).

 ## Installation
 
 ```bash
 npm install exospherehost

+Requirements:
+- Node.js >= 18.17 (built-in fetch) or provide a fetch polyfill.
+Optional environment variables (can also be passed via options):
+- EXOSPHERE_STATE_MANAGER_URI
+- EXOSPHERE_API_KEY


---

`63-76`: **runtime.start() is long-running; note for CLI usage**

Avoid accidental indefinite awaits in scripts.  
 

```diff
-await runtime.start();
+// Long-running: consider not awaiting in short-lived scripts
+await runtime.start();

37-61: Show explicit generics when extending BaseNode

Improves clarity of Input/Output/Secrets types.

-class SampleNode extends BaseNode {
+class SampleNode extends BaseNode<
+  typeof SampleNode.Inputs,
+  typeof SampleNode.Outputs,
+  typeof SampleNode.Secrets
+> {

Comment on lines +291 to +295

private needSecrets(node: typeof BaseNode) {
return Object.keys((node.Secrets as ZodObject<any>).shape).length > 0;
}

Copy link
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick

Type tightening: accept constructor type

Optional, but aligns with usage in needSecrets/new.

-  private needSecrets(node: typeof BaseNode) {
+  private needSecrets(node: NodeCtor) {
     return Object.keys((node.Secrets as ZodObject<any>).shape).length > 0;
   }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
private needSecrets(node: typeof BaseNode) {
return Object.keys((node.Secrets as ZodObject<any>).shape).length > 0;
}
private needSecrets(node: NodeCtor) {
return Object.keys((node.Secrets as ZodObject<any>).shape).length > 0;
}

Comment on lines +1 to +6
# ExosphereHost TypeScript SDK

[![npm version](https://badge.fury.io/js/exospherehost.svg)](https://badge.fury.io/js/exospherehost)
[![Node.js 18+](https://img.shields.io/badge/node.js-18+-green.svg)](https://nodejs.org/)
[![TypeScript](https://img.shields.io/badge/TypeScript-5.0+-blue.svg)](https://www.typescriptlang.org/)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
Copy link
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick

Optional: document stable NodeName for bundlers

Matches runtime change to prefer static NodeName over class name.

 class GreetingNode extends BaseNode {
-  static name = 'greeting';
+  static name = 'greeting';
+  // Optional: stable identifier if minifiers mangle names
+  static NodeName = 'greeting';

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In typescript-sdk/README.md around lines 1–6, add a short section documenting
the new runtime behavior that prefers a static NodeName over using the class
name so bundlers can reliably identify node types; state that Node
implementations should export or declare a stable static NodeName property (and
show the intended usage pattern and example value), explain how bundlers should
reference that static property instead of relying on class.name, and add a brief
compatibility note about older code that relied on class names and how to
migrate.

Comment on lines +407 to +428
// Create a single trigger state
const triggerState = {
identifier: "user-login",
inputs: {
"user_id": "12345",
"session_token": "abc123def456",
"timestamp": "2024-01-15T10:30:00Z"
}
};

// Trigger the graph
const result = await stateManager.trigger(
"my-graph",
{
"user_id": "12345",
"session_token": "abc123def456"
},
{
"cursor": "0" // persisted across nodes
}
);
```
Copy link
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick

Remove unused triggerState example or hook it up

Currently defined then unused; can confuse users.

-// Create a single trigger state
-const triggerState = {
-  identifier: "user-login",
-  inputs: {
-    "user_id": "12345",
-    "session_token": "abc123def456",
-    "timestamp": "2024-01-15T10:30:00Z"
-  }
-};
-
 // Trigger the graph 
 const result = await stateManager.trigger(
   "my-graph",
   {
     "user_id": "12345",
     "session_token": "abc123def456"
   },
   {
     "cursor": "0" // persisted across nodes
   }
 );
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Create a single trigger state
const triggerState = {
identifier: "user-login",
inputs: {
"user_id": "12345",
"session_token": "abc123def456",
"timestamp": "2024-01-15T10:30:00Z"
}
};
// Trigger the graph
const result = await stateManager.trigger(
"my-graph",
{
"user_id": "12345",
"session_token": "abc123def456"
},
{
"cursor": "0" // persisted across nodes
}
);
```
// Trigger the graph
const result = await stateManager.trigger(
"my-graph",
{
"user_id": "12345",
"session_token": "abc123def456"
},
{
"cursor": "0" // persisted across nodes
}
);
🤖 Prompt for AI Agents
In typescript-sdk/README.md around lines 407 to 428, the example defines a const
triggerState that is never used which can confuse readers; either remove the
unused triggerState block entirely, or update the example to pass its values
into the trigger call (e.g., use triggerState.inputs or merge triggerState into
the second argument) so the variable is consumed and the example demonstrates a
real flow.

- Introduced a new workflow to automate the build, test, and publish process for the TypeScript SDK on npm.
- Configured jobs for testing, publishing, and creating GitHub releases, including steps for dependency installation, TypeScript compilation, and test coverage reporting.
- Enhanced package.json with prepublish and prepack scripts for better version management.
- Updated .gitignore to exclude additional build artifacts and logs.
- Added version management scripts for easier version bumping (beta, patch, minor, major).
- Adjusted test expectations to handle null results appropriately.
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 14

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
.github/workflows/publish-typescript-sdk.yml (1)

160-185: Optional: Prefer maintained release action and attach artifacts.

Consider softprops/action-gh-release@v1 and attach SBOM/vuln report for traceability.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f559144 and 85621fd.

📒 Files selected for processing (9)
  • .github/workflows/publish-typescript-sdk.yml (1 hunks)
  • typescript-sdk/.gitignore (1 hunks)
  • typescript-sdk/exospherehost/node/BaseNode.ts (1 hunks)
  • typescript-sdk/package.json (1 hunks)
  • typescript-sdk/scripts/version.bat (1 hunks)
  • typescript-sdk/scripts/version.js (1 hunks)
  • typescript-sdk/tests/test_base_node_comprehensive.test.ts (1 hunks)
  • typescript-sdk/tests/test_runtime_comprehensive.test.ts (1 hunks)
  • typescript-sdk/tests/test_signals_and_runtime_functions.test.ts (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (3)
typescript-sdk/tests/test_base_node_comprehensive.test.ts (1)
typescript-sdk/exospherehost/utils.ts (2)
  • isZodObjectSchema (5-14)
  • isZodStringSchema (16-25)
typescript-sdk/tests/test_signals_and_runtime_functions.test.ts (2)
typescript-sdk/exospherehost/signals.ts (2)
  • PruneSignal (1-20)
  • ReQueueAfterSignal (22-44)
typescript-sdk/exospherehost/runtime.ts (1)
  • Runtime (58-357)
typescript-sdk/tests/test_runtime_comprehensive.test.ts (1)
typescript-sdk/exospherehost/runtime.ts (1)
  • Runtime (58-357)
🪛 YAMLlint (1.37.1)
.github/workflows/publish-typescript-sdk.yml

[warning] 3-3: truthy value should be one of [false, true]

(truthy)

🔇 Additional comments (20)
typescript-sdk/.gitignore (2)

1-5: Deduplicate and normalize directory ignores

You already include the normalized entries with trailing slashes. Remove the redundant/unnecessary variants to avoid drift.

-node_modules
-Dist
-dist

6-15: Add a few standard ignores (env, caches, editor, more logs)

These are common for TS SDKs and prevent accidental commits of secrets and local noise.

 # build cache / type info
 *.tsbuildinfo
 # coverage / vitest
 coverage/
 .vitest/
 .vite/
 # logs & OS junk
 npm-debug.log*
 yarn-error.log*
 .DS_Store
+*.log
+pnpm-debug.log*
+
+# env files (keep .env.example committed if present)
+.env
+.env.local
+.env.*.local
+
+# editor/caches
+.eslintcache
+.cache/
+.idea/
+.vscode/
+
+# nyc/istanbul output
+.nyc_output/
+
+# Windows
+Thumbs.db
typescript-sdk/tests/test_signals_and_runtime_functions.test.ts (3)

28-37: Add per-test mock reset in TestPruneSignal.

Ensure isolation; clear fetch mocks before each test in this suite.

 describe('TestPruneSignal', () => {
+  beforeEach(() => {
+    vi.clearAllMocks();
+  });
   it('should initialize with data', () => {

82-91: Add per-test mock reset in TestReQueueAfterSignal.

Same isolation need as other suites.

 describe('TestReQueueAfterSignal', () => {
+  beforeEach(() => {
+    vi.clearAllMocks();
+  });
   it('should initialize with delay', () => {

82-91: Also assert the interpolated delay value is present.

Improve the message assertion by checking the numeric delay.

   it('should initialize with delay', () => {
     const delayMs = 30000;
     const signal = new ReQueueAfterSignal(delayMs);
     
     expect(signal.delayMs).toBe(delayMs);
     expect(signal.message).toContain('ReQueueAfter signal received with delay');
+    expect(signal.message).toContain(`${delayMs}ms`);
     expect(signal.message).toContain('Do not catch this Exception');
   });
typescript-sdk/exospherehost/node/BaseNode.ts (2)

17-34: Type contract: allow null instead of returning {}.

Returning {} for null results is misleading and skirts type safety. Include null in the return types and propagate it.

-  async _execute(inputsRaw: unknown, secretsRaw: unknown): Promise<z.infer<O> | z.infer<O>[]> {
+  async _execute(inputsRaw: unknown, secretsRaw: unknown): Promise<z.infer<O> | z.infer<O>[] | null> {
@@
-    if (result === null) {
-      return {} as z.infer<O>;
-    }
+    if (result === null) {
+      return null;
+    }
     return outputsSchema.parse(result);
   }
@@
-  abstract execute(): Promise<z.infer<O> | z.infer<O>[]>;
+  abstract execute(): Promise<z.infer<O> | z.infer<O>[] | null>;

Follow-ups:

  • Update tests expecting {} for “no output” to expect null.
  • In runtime, handle null by skipping notify or sending an empty outputs array (confirm expected API behavior).
    Run repo-wide checks to find affected call sites.
#!/bin/bash
rg -nP --type=ts '\._execute\(' -C2

17-24: Guard against reentrancy on the same instance.

Concurrent _execute calls can race on this.inputs/this.secrets. Serialize executions on an instance.

 export abstract class BaseNode<I extends ZodObject<any> = ZodObject<any>, O extends ZodObject<any> = ZodObject<any>, S extends ZodObject<any> = ZodObject<any>> {
@@
   protected secrets!: z.infer<S>;
+  private _executing = false;
@@
-  async _execute(inputsRaw: unknown, secretsRaw: unknown): Promise<z.infer<O> | z.infer<O>[]> {
+  async _execute(inputsRaw: unknown, secretsRaw: unknown): Promise<z.infer<O> | z.infer<O>[] | null> {
+    if (this._executing) {
+      throw new Error('BaseNode instances are not re-entrant; create a new instance per execution.');
+    }
+    this._executing = true;
     const ctor = this.constructor as typeof BaseNode;
     const inputs = (ctor.Inputs as I).parse(inputsRaw);
     const secrets = (ctor.Secrets as S).parse(secretsRaw);
     this.inputs = inputs;
     this.secrets = secrets;
-    const result = await this.execute();
-    const outputsSchema = ctor.Outputs as O;
-    if (Array.isArray(result)) {
-      return result.map(r => outputsSchema.parse(r));
-    }
-    if (result === null) {
-      return {} as z.infer<O>;
-    }
-    return outputsSchema.parse(result);
+    try {
+      const result = await this.execute();
+      const outputsSchema = ctor.Outputs as O;
+      if (Array.isArray(result)) {
+        return result.map(r => outputsSchema.parse(r));
+      }
+      if (result === null) {
+        return null;
+      }
+      return outputsSchema.parse(result);
+    } finally {
+      this._executing = false;
+    }
   }

Also applies to: 31-34

typescript-sdk/tests/test_runtime_comprehensive.test.ts (5)

6-8: Centralize global.fetch mocking and reset lifecycle.

Avoid per-file assignment; move to tests/setup.ts and reset between tests to prevent interference.

-// Mock fetch globally
-global.fetch = vi.fn();
+// tests/setup.ts
+import { vi } from 'vitest';
+global.fetch = vi.fn();

Then add in suites:

beforeEach(() => { vi.clearAllMocks(); });

41-42: Specify radix for parseInt.

Make parsing deterministic.

-    const count = parseInt(this.inputs.count);
+    const count = parseInt(this.inputs.count, 10);

201-204: Brittle error assertion; use regex.

The error includes server text; match the prefix.

-    await expect((runtime as any).register()).rejects.toThrow('Failed to register nodes');
+    await expect((runtime as any).register()).rejects.toThrow(/Failed to register nodes/);

267-273: Worker is an infinite loop; constrain to one iteration in tests.

Don’t await the loop and park after one state to avoid async leaks.

-    // Start worker and let it process one item
-    const workerPromise = (runtime as any).worker(1);
+    // Drive a single iteration, then park subsequent gets
+    const originalGet = (runtime as any).stateQueue.get.bind((runtime as any).stateQueue);
+    vi.spyOn((runtime as any).stateQueue, 'get')
+      .mockImplementationOnce(originalGet)
+      .mockImplementation(() => new Promise(() => {})); // park forever
+    (runtime as any).worker(1); // do not await

Apply similarly to the other worker tests in this file.

Also applies to: 296-301, 319-324, 342-347


287-288: Remove unused register() spies.

These spies aren’t asserted; drop them to reduce noise.

-    vi.spyOn(runtime as any, 'register').mockResolvedValue({ status: 'registered' });

Also applies to: 310-311, 333-334

typescript-sdk/tests/test_base_node_comprehensive.test.ts (2)

44-46: Specify radix for parseInt.

Ensure deterministic parsing.

-    const count = parseInt(this.inputs.items);
+    const count = parseInt(this.inputs.items, 10);

344-351: Concurrent _execute on the same instance races internal state.

Create a new instance per concurrent run (or add non-reentrancy guard in BaseNode as suggested).

-    const node = new ValidNode();
-    const inputs = { name: 'test', count: '1' };
-    const secrets = { api_key: 'key', token: 'token' };
-    const promises = Array.from({ length: 5 }, () => node._execute(inputs, secrets));
+    const inputs = { name: 'test', count: '1' };
+    const secrets = { api_key: 'key', token: 'token' };
+    const promises = Array.from({ length: 5 }, () => new ValidNode()._execute(inputs, secrets));
typescript-sdk/package.json (6)

64-65: Raise engines floor to Node 18.17+ (stable fetch/Web APIs)

18.0 has rough edges; 18.17+ is a safer baseline for global fetch/streams.

   "engines": {
-    "node": ">=18.0.0"
+    "node": ">=18.17.0"
   },

6-7: Ensure dist entry points exist before publish (and aggregator builds)

package.json points to dist/index.{js,d.ts}; verify they are produced by tsc and included in the tarball. Add/keep "prepare" to build on git installs.

#!/bin/bash
set -euo pipefail
test -f dist/index.js && echo "OK: dist/index.js" || (echo "MISSING: dist/index.js" && exit 1)
test -f dist/index.d.ts && echo "OK: dist/index.d.ts" || (echo "MISSING: dist/index.d.ts" && exit 1)
npm pack --dry-run | sed -n '1,200p'

26-36: Peer dep setup looks good; mark @types/node as optional

Keeps flexibility for consumers without ambient Node types. Keeping zod in devDependencies alongside peerDependencies is fine.

   "peerDependencies": {
     "@types/node": ">=20.0.0",
     "zod": "^3.23.8"
   },
+  "peerDependenciesMeta": {
+    "@types/node": {
+      "optional": true
+    }
+  },

8-22: Build on git installs and add a typecheck script

"prepare" builds for git-based installs; explicit "typecheck" helps CI.

   "scripts": {
     "build": "tsc",
+    "typecheck": "tsc -p tsconfig.json --noEmit",
     "test": "vitest",
     "test:run": "vitest run",
     "test:coverage": "vitest run --coverage",
-    "prepublishOnly": "npm run build && npm run test:run",
+    "prepublishOnly": "npm run typecheck && npm run build && npm run test:run",
     "prepack": "npm run build",
+    "prepare": "npm run build",
     "clean": "rm -rf dist",
     "lint": "echo 'No linting configured'",
     "lint:fix": "echo 'No linting configured'",

6-7: Add an exports map and mark the package as side‑effect free

Locks public surface, blocks deep imports, and improves tree‑shaking.

   "type": "module",
   "main": "dist/index.js",
   "types": "dist/index.d.ts",
+  "exports": {
+    ".": {
+      "types": "./dist/index.d.ts",
+      "import": "./dist/index.js"
+    }
+  },
+  "sideEffects": false,

23-25: Move zod-to-json-schema to devDependencies

No runtime imports/usages found in the repo; typescript-sdk/package.json currently lists "zod-to-json-schema" under dependencies. If it's only used for build/test, move it to devDependencies to avoid adding it to consumers' installs — confirm there are no dynamic/runtime imports or runtime JSON‑Schema generation first.

Comment on lines +66 to +74
publish:
runs-on: ubuntu-latest
needs: test
defaults:
run:
working-directory: typescript-sdk
if: github.repository == 'exospherehost/exospherehost'

steps:
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Prevent double publishing; gate publish job to push events only.

Currently runs on release events too (and release job also publishes), risking duplicate publish failures.

   publish:
@@
-    if: github.repository == 'exospherehost/exospherehost'
+    if: github.event_name == 'push' && github.repository == 'exospherehost/exospherehost'
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
publish:
runs-on: ubuntu-latest
needs: test
defaults:
run:
working-directory: typescript-sdk
if: github.repository == 'exospherehost/exospherehost'
steps:
publish:
runs-on: ubuntu-latest
needs: test
defaults:
run:
working-directory: typescript-sdk
if: github.event_name == 'push' && github.repository == 'exospherehost/exospherehost'
steps:
🤖 Prompt for AI Agents
In .github/workflows/publish-typescript-sdk.yml around lines 66 to 74, the
publish job is currently gated only by the repository name and thus runs on
release events as well, causing duplicate publish attempts; change the job-level
if condition to only run on push events for this repository (e.g., require
github.event_name == 'push' in addition to the repository check) so the publish
job is skipped for release events and only executes on push events to
exospherehost/exospherehost.

Comment on lines +92 to +101
- name: Check version for beta indicator
run: |
VERSION=$(node -p "require('./package.json').version")
if [[ "$VERSION" == *"b"* ]]; then
echo "Version $VERSION contains beta indicator - publishing to npm with beta tag"
echo "NPM_TAG=beta" >> $GITHUB_ENV
else
echo "Version $VERSION does not contain beta indicator - publishing to npm with latest tag"
echo "NPM_TAG=latest" >> $GITHUB_ENV
fi
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Fix beta tag detection; matching any “b” is unsafe.

Use prerelease detection consistent with semver -beta.N (after version.js refactor).

-      - name: Check version for beta indicator
+      - name: Determine npm tag from semver prerelease
         run: |
-          VERSION=$(node -p "require('./package.json').version")
-          if [[ "$VERSION" == *"b"* ]]; then
-            echo "Version $VERSION contains beta indicator - publishing to npm with beta tag"
-            echo "NPM_TAG=beta" >> $GITHUB_ENV
-          else
-            echo "Version $VERSION does not contain beta indicator - publishing to npm with latest tag"
-            echo "NPM_TAG=latest" >> $GITHUB_ENV
-          fi
+          VERSION=$(node -p "require('./package.json').version")
+          if [[ "$VERSION" == *"-beta."* ]]; then
+            echo "NPM_TAG=beta" >> "$GITHUB_ENV"
+          elif [[ "$VERSION" == *"-"* ]]; then
+            # Other prereleases (alpha/rc) -> publish under 'next'
+            echo "NPM_TAG=next" >> "$GITHUB_ENV"
+          else
+            echo "NPM_TAG=latest" >> "$GITHUB_ENV"
+          fi
🤖 Prompt for AI Agents
.github/workflows/publish-typescript-sdk.yml around lines 92-101: the current
check looks for any "b" in the version which is too permissive; replace it with
a prerelease-aware check that matches semver beta identifiers (e.g. -beta or
-beta.N). Update the script to set VERSION=$(node -p
"require('./package.json').version") and use a bash regex such as if [[
"$VERSION" =~ -beta([[:digit:]]+)?($|\\.) ]]; then ... to detect "-beta" or
"-beta.N" (and set NPM_TAG=beta), otherwise set NPM_TAG=latest.

Comment on lines +154 to +158
- name: Publish to npm with latest tag
run: npm publish --tag latest --access public
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}

Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Avoid republishing on release event.

Publishing already happens in publish on push; remove duplicate publish here to prevent 409 errors on identical version.

-      - name: Publish to npm with latest tag
-        run: npm publish --tag latest --access public
-        env:
-          NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
+      # Publishing happens in the 'publish' job on push. Skip duplicate publish here.
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
- name: Publish to npm with latest tag
run: npm publish --tag latest --access public
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
# Publishing happens in the 'publish' job on push. Skip duplicate publish here.
🤖 Prompt for AI Agents
.github/workflows/publish-typescript-sdk.yml around lines 154 to 158: the
workflow contains a duplicate "Publish to npm with latest tag" job step that
re-publishes on release events and can cause 409 errors for identical versions;
remove this npm publish step (the name/run/env block) from these lines so
publishing only occurs in the existing publish-on-push job, or alternatively
gate it behind a conditional that skips publishing when the version/tag already
exists.

@@ -0,0 +1,70 @@
{
"name": "exospherehost",
Copy link
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick

🧩 Analysis chain

Consider a scoped package name before first publish

Prefer "@exospherehost/sdk" (or similar) to avoid collisions and to convey ownership. Verify "exospherehost" availability on npm before committing to this unscoped name.


🏁 Script executed:

#!/bin/bash
set -euo pipefail
echo "Checking npm availability for 'exospherehost'..."
npm view exospherehost name version description || echo "Looks available (no npm metadata found)."
echo "If publishing under a scope, consider: @exospherehost/sdk"

Length of output: 558


Consider using a scoped package name; 'exospherehost' appears available on npm (checked 2025-09-22)
npm view returned 404, so the unscoped name looks free; prefer @exospherehost/sdk to avoid future collisions.
File: typescript-sdk/package.json (name)

🤖 Prompt for AI Agents
In typescript-sdk/package.json around line 2, the package name is currently
unscoped ("exospherehost"); change it to a scoped name (e.g.
"@exospherehost/sdk") by updating the "name" field to the chosen scope, ensure
the JSON remains valid, update any internal references or README/docs that
mention the old name, and verify npm publish settings (access/public) and
CI/publish workflows are updated to publish the scoped package.

Comment on lines +116 to +127
beta Create a beta version (e.g., 0.1.0b1, 0.1.0b2)
patch Create a patch version (e.g., 0.1.0 → 0.1.1)
minor Create a minor version (e.g., 0.1.0 → 0.2.0)
major Create a major version (e.g., 0.1.0 → 1.0.0)

Examples:
node scripts/version.js beta # 0.1.0 → 0.1.0b1
node scripts/version.js patch # 0.1.0b1 → 0.1.0
node scripts/version.js minor # 0.1.0 → 0.2.0
node scripts/version.js major # 0.1.0 → 1.0.0

Current version: ${getCurrentVersion()}
Copy link
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick

Update help text to reflect valid prerelease format.

Docs/examples should show -beta.N not bN.

-  beta    Create a beta version (e.g., 0.1.0b1, 0.1.0b2)
+  beta    Create a beta version (e.g., 0.1.0-beta.1, 0.1.0-beta.2)
@@
-  node scripts/version.js beta    # 0.1.0 → 0.1.0b1
-  node scripts/version.js patch   # 0.1.0b1 → 0.1.0
+  node scripts/version.js beta    # 0.1.0 → 0.1.0-beta.1
+  node scripts/version.js patch   # 0.1.0-beta.1 → 0.1.0
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
beta Create a beta version (e.g., 0.1.0b1, 0.1.0b2)
patch Create a patch version (e.g., 0.1.0 0.1.1)
minor Create a minor version (e.g., 0.1.0 0.2.0)
major Create a major version (e.g., 0.1.0 1.0.0)
Examples:
node scripts/version.js beta # 0.1.0 0.1.0b1
node scripts/version.js patch # 0.1.0b1 0.1.0
node scripts/version.js minor # 0.1.0 0.2.0
node scripts/version.js major # 0.1.0 1.0.0
Current version: ${getCurrentVersion()}
beta Create a beta version (e.g., 0.1.0-beta.1, 0.1.0-beta.2)
patch Create a patch version (e.g., 0.1.0 0.1.1)
minor Create a minor version (e.g., 0.1.0 0.2.0)
major Create a major version (e.g., 0.1.0 1.0.0)
Examples:
node scripts/version.js beta # 0.1.0 0.1.0-beta.1
node scripts/version.js patch # 0.1.0-beta.1 0.1.0
node scripts/version.js minor # 0.1.0 0.2.0
node scripts/version.js major # 0.1.0 1.0.0
Current version: ${getCurrentVersion()}
🤖 Prompt for AI Agents
In typescript-sdk/scripts/version.js around lines 116 to 127, the help text and
examples currently show prerelease versions using the `bN` format (e.g.,
0.1.0b1) but should use the `-beta.N` format; update the descriptive lines and
all example strings to use `-beta.N` (e.g., 0.1.0 → 0.1.0-beta.1 and
0.1.0-beta.1 → 0.1.0) and adjust the shorthand example invocation comment
accordingly so the docs and examples consistently reflect the valid prerelease
format.

Comment on lines +182 to +191
it('should handle node with null output', async () => {
const node = new NodeWithNoneOutput();
const inputs = { name: 'test' };
const secrets = { api_key: 'test_key' };

const result = await node._execute(inputs, secrets);

expect(result).toEqual({});
expect((node as any).inputs).toEqual(inputs);
expect((node as any).secrets).toEqual(secrets);
Copy link
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick

Note on “null output” expectations.

Tests assert {} when execute() returns null. If BaseNode is updated to return null (recommended), adjust these tests to expect null.

Also applies to: 430-436

🤖 Prompt for AI Agents
In typescript-sdk/tests/test_base_node_comprehensive.test.ts around lines
182-191 (and also update the similar assertions at lines 430-436), the test
currently expects {} when a node returns null; change the assertions to expect
null instead: update the result expectation from expect(result).toEqual({}) to
expect(result).toBeNull() (or expect(result).toEqual(null)), and keep the input
and secret checks unchanged so the test reflects BaseNode returning null.

Comment on lines +7 to +8
// Mock fetch globally
global.fetch = vi.fn();
Copy link
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick

Centralize global.fetch mocking and reset per test.

Move the global fetch mock to a shared test setup (e.g., tests/setup.ts) and ensure beforeEach resets to avoid cross-file bleed.

Example:

-// Mock fetch globally
-global.fetch = vi.fn();
+// tests/setup.ts
+import { vi } from 'vitest';
+global.fetch = vi.fn();

+// vitest.config.ts
+test: { setupFiles: ['tests/setup.ts'] }

And add per-suite:

beforeEach(() => { vi.clearAllMocks(); });
🤖 Prompt for AI Agents
In typescript-sdk/tests/test_signals_and_runtime_functions.test.ts around lines
7-8, the global.fetch mock is declared directly in this test file causing
cross-file bleed; move the global.fetch = vi.fn() into a shared setup file
(e.g., tests/setup.ts) that is registered in the test runner config, remove the
direct mock from this test, and ensure each suite calls beforeEach(() => {
vi.clearAllMocks(); }) (or add a global beforeEach in tests/setup.ts) so mocks
are reset between tests.

Comment on lines +70 to +79
it('should handle send failure', async () => {
(global.fetch as any).mockResolvedValueOnce({
ok: false
});

const data = { reason: 'test_prune' };
const signal = new PruneSignal(data);

await expect(signal.send('http://test-endpoint/prune', 'test-api-key')).rejects.toThrow('Failed to send prune signal');
});
Copy link
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick

Make failure assertion resilient to appended details.

The thrown message includes the endpoint; match by regex.

-    await expect(signal.send('http://test-endpoint/prune', 'test-api-key')).rejects.toThrow('Failed to send prune signal');
+    await expect(signal.send('http://test-endpoint/prune', 'test-api-key')).rejects.toThrow(/Failed to send prune signal/);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
it('should handle send failure', async () => {
(global.fetch as any).mockResolvedValueOnce({
ok: false
});
const data = { reason: 'test_prune' };
const signal = new PruneSignal(data);
await expect(signal.send('http://test-endpoint/prune', 'test-api-key')).rejects.toThrow('Failed to send prune signal');
});
it('should handle send failure', async () => {
(global.fetch as any).mockResolvedValueOnce({
ok: false
});
const data = { reason: 'test_prune' };
const signal = new PruneSignal(data);
await expect(signal.send('http://test-endpoint/prune', 'test-api-key')).rejects.toThrow(/Failed to send prune signal/);
});
🤖 Prompt for AI Agents
In typescript-sdk/tests/test_signals_and_runtime_functions.test.ts around lines
70 to 79, the test asserts the exact thrown message 'Failed to send prune
signal' but the implementation appends details like the endpoint, so update the
expectation to match the error using a regex (e.g. /Failed to send prune
signal/) instead of exact string equality; change the
expect(...).rejects.toThrow call to use a regex matcher that will pass whether
or not extra context is appended.

Comment on lines +137 to +146
it('should handle send failure', async () => {
(global.fetch as any).mockResolvedValueOnce({
ok: false
});

const delayMs = 30000;
const signal = new ReQueueAfterSignal(delayMs);

await expect(signal.send('http://test-endpoint/requeue', 'test-api-key')).rejects.toThrow('Failed to send requeue after signal');
});
Copy link
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick

Make failure assertion resilient to appended details.

Same reason as prune failure path.

-    await expect(signal.send('http://test-endpoint/requeue', 'test-api-key')).rejects.toThrow('Failed to send requeue after signal');
+    await expect(signal.send('http://test-endpoint/requeue', 'test-api-key')).rejects.toThrow(/Failed to send requeue after signal/);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
it('should handle send failure', async () => {
(global.fetch as any).mockResolvedValueOnce({
ok: false
});
const delayMs = 30000;
const signal = new ReQueueAfterSignal(delayMs);
await expect(signal.send('http://test-endpoint/requeue', 'test-api-key')).rejects.toThrow('Failed to send requeue after signal');
});
it('should handle send failure', async () => {
(global.fetch as any).mockResolvedValueOnce({
ok: false
});
const delayMs = 30000;
const signal = new ReQueueAfterSignal(delayMs);
await expect(signal.send('http://test-endpoint/requeue', 'test-api-key')).rejects.toThrow(/Failed to send requeue after signal/);
});
🤖 Prompt for AI Agents
In typescript-sdk/tests/test_signals_and_runtime_functions.test.ts around lines
137 to 146, the test asserts the exact error message on send failure which can
break if additional details are appended; change the assertion to be resilient
by expecting a partial match (e.g., use toThrow with a RegExp or
toThrowErrorMatchingInlineSnapshot that matches /Failed to send requeue after
signal/) so the test passes even when extra context is added to the error
message.

nk-ag and others added 3 commits September 22, 2025 08:22
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 5

📜 Review details

Configuration used: CodeRabbit UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 85621fd and 0cc1335.

📒 Files selected for processing (2)
  • typescript-sdk/exospherehost/types.ts (1 hunks)
  • typescript-sdk/tests/test_models_and_statemanager_new.test.ts (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
typescript-sdk/tests/test_models_and_statemanager_new.test.ts (1)
typescript-sdk/exospherehost/models.ts (6)
  • GraphNodeModel (18-65)
  • GraphNodeModel (67-67)
  • StoreConfigModel (94-157)
  • StoreConfigModel (159-159)
  • RetryPolicyModel (83-89)
  • RetryPolicyModel (91-91)
🔇 Additional comments (3)
typescript-sdk/exospherehost/types.ts (1)

6-12: Past mismatch resolved: next_nodes is optional.

This aligns the interface with the model schema. Good catch.

typescript-sdk/tests/test_models_and_statemanager_new.test.ts (2)

25-36: Avoid null for optional fields to keep failures deterministic.

Passing null to optional fields (next_nodes, unites) triggers type errors that can mask the intended validation. Omit these keys or use empty values as appropriate.

Apply this diff:

@@
   it('should validate node name cannot be empty', () => {
     expect(() => {
       GraphNodeModel.parse({
         node_name: '  ',
         namespace: 'ns',
         identifier: 'id1',
         inputs: {},
-        next_nodes: null,
-        unites: null
+        // omit optional fields to isolate the node_name failure
+        // next_nodes omitted
+        // unites omitted
       });
     }).toThrow('Node name cannot be empty');
   });
@@
       GraphNodeModel.parse({
         node_name: 'n',
         namespace: 'ns',
         identifier: 'id1',
         inputs: {},
-        next_nodes: ['', 'id2'],
-        unites: null
+        next_nodes: ['', 'id2']
+        // unites omitted
       });
     }).toThrow('cannot be empty');
@@
       GraphNodeModel.parse({
         node_name: 'n',
         namespace: 'ns',
         identifier: 'id1',
         inputs: {},
-        next_nodes: ['dup', 'dup'],
-        unites: null
+        next_nodes: ['dup', 'dup']
+        // unites omitted
       });
     }).toThrow('not unique');
@@
       GraphNodeModel.parse({
         node_name: 'n',
         namespace: 'ns',
         identifier: 'id1',
         inputs: {},
-        next_nodes: null,
+        // next_nodes omitted
         unites: { identifier: '  ' }
       });
     }).toThrow('Unites identifier cannot be empty');

Also applies to: 58-60, 71-73, 84-86


25-36: Make Zod error assertions robust with safeParse + issues inspection.

String-matching thrown messages is brittle. Prefer safeParse and assert on error.issues (message + path).

Example refactors:

@@
-  it('should validate node name cannot be empty', () => {
-    expect(() => {
-      GraphNodeModel.parse({
-        node_name: '  ',
-        namespace: 'ns',
-        identifier: 'id1',
-        inputs: {},
-        // optional fields omitted per previous comment
-      });
-    }).toThrow('Node name cannot be empty');
-  });
+  it('should validate node name cannot be empty', () => {
+    const result = GraphNodeModel.safeParse({
+      node_name: '  ',
+      namespace: 'ns',
+      identifier: 'id1',
+      inputs: {}
+    });
+    expect(result.success).toBe(false);
+    expect(result.error!.issues).toEqual(
+      expect.arrayContaining([
+        expect.objectContaining({ message: 'Node name cannot be empty', path: ['node_name'] })
+      ])
+    );
+  });
@@
-  it('should validate identifier is not reserved word', () => {
-    expect(() => {
-      GraphNodeModel.parse({
-        node_name: 'n',
-        namespace: 'ns',
-        identifier: 'store',
-        inputs: {},
-        next_nodes: [],
-        unites: undefined
-      });
-    }).toThrow('reserved word');
-  });
+  it('should validate identifier is not reserved word', () => {
+    const result = GraphNodeModel.safeParse({
+      node_name: 'n',
+      namespace: 'ns',
+      identifier: 'store',
+      inputs: {},
+      next_nodes: [],
+      unites: undefined
+    });
+    expect(result.success).toBe(false);
+    expect(result.error!.issues).toEqual(
+      expect.arrayContaining([
+        expect.objectContaining({
+          message: "Node identifier cannot be reserved word 'store'",
+          path: ['identifier']
+        })
+      ])
+    );
+  });
@@
-  it('should validate duplicated keys', () => {
-    expect(() => {
-      StoreConfigModel.parse({
-        required_keys: ['a', 'a']
-      });
-    }).toThrow('duplicated');
-  });
+  it('should validate duplicated keys', () => {
+    const result = StoreConfigModel.safeParse({ required_keys: ['a', 'a'] });
+    expect(result.success).toBe(false);
+    expect(result.error!.issues.some(i => /duplicated/.test(i.message) && i.path[0] === 'required_keys')).toBe(true);
+  });

If you prefer to keep toThrow, at least use a regex that matches the actual message, e.g. toThrow(/Node identifier .* reserved word 'store'/).

Also applies to: 38-49, 51-62, 64-75, 77-88, 102-107, 110-116, 118-124, 126-132, 134-141

Comment on lines +1 to +4
export interface TriggerState {
identifier: string;
inputs: Record<string, string>;
}
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

🧩 Analysis chain

Unify inputs typing across SDK; prefer a JSONValue.

GraphNode.inputs allows unknown while TriggerState.inputs forces string. This divergence leaks into the public API and complicates reuse. Recommend a single JSON-serializable value type used in both.

+export type JSONValue =
+  | string
+  | number
+  | boolean
+  | null
+  | { [key: string]: JSONValue }
+  | JSONValue[];
+
 export interface TriggerState {
   identifier: string;
-  inputs: Record<string, string>;
+  inputs: Record<string, JSONValue>;
 }
@@
 export interface GraphNode {
   node_name: string;
   identifier: string;
-  inputs: Record<string, unknown>;
+  inputs: Record<string, JSONValue>;
   next_nodes?: string[];
   namespace?: string;
 }

Please confirm other modules don’t rely on TriggerState.inputs being strictly string.


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== typescript-sdk/exospherehost/types.ts (first 200 lines) ==="
sed -n '1,200p' typescript-sdk/exospherehost/types.ts || true

echo
echo "=== rg: usages of 'TriggerState' (TypeScript files, context) ==="
rg -n --type=ts -C2 '\bTriggerState\b' || true

echo
echo "=== rg: usages of 'GraphNode' (TypeScript files, context) ==="
rg -n --type=ts -C2 '\bGraphNode\b' || true

echo
echo "=== rg: occurrences of 'Record<string, string>' (TypeScript files, context) ==="
rg -n --type=ts -C2 'Record<\s*string\s*,\s*string\s*>' || true

echo
echo "=== rg: occurrences of 'inputs' property (TypeScript files, context) ==="
rg -n --type=ts -C2 '\binputs\b' || true

echo
echo "=== rg: imports that reference exospherehost/types (TS files, context) ==="
rg -n --type=ts -C2 'exospherehost/types' || true

Length of output: 56690


Do not change TriggerState.inputs to JSONValue without a coordinated refactor — callers expect strings.

  • Evidence: callers use Record<string,string> (typescript-sdk/exospherehost/stateManager.ts — trigger signature; typescript-sdk/exospherehost/runtime.ts — node state, getSecrets, and runtime pass Record<string,string>; typescript-sdk/exospherehost/models.ts — z.record(z.string()) / default_values transform; many tests under typescript-sdk/tests construct string-valued inputs).
  • Action: either (A) adopt JSONValue and update all callsites, Zod schemas, models, runtime and tests, or (B) keep TriggerState.inputs as Record<string,string> and only unify types where safe.
🤖 Prompt for AI Agents
In typescript-sdk/exospherehost/types.ts around lines 1 to 4, do not change
TriggerState.inputs to a JSONValue; keep it explicitly typed as Record<string,
string> to match existing callers, Zod schemas, models and tests. If a broader
change to JSONValue is desired, coordinate a full refactor: update all
callsites, Zod schemas, models, runtime functions, and tests together —
otherwise preserve Record<string,string> and add a short inline comment
explaining why it must remain string-valued to prevent accidental mass
replacement.

Comment on lines +102 to +107
it('should validate duplicated keys', () => {
expect(() => {
StoreConfigModel.parse({
required_keys: ['a', 'a']
});
}).toThrow('duplicated');
Copy link
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick

Deduplicate negative-path store-config tests via test.each.

Consolidates repeated structure and makes adding new cases trivial.

Example:

describe('StoreConfigModel invalid cases', () => {
  it.each([
    [{ required_keys: ['a', 'a'] }, /duplicated/],
    [{ required_keys: ['a.'] }, /cannot contain '\.'/],
    [{ required_keys: ['  '] }, /cannot be empty/],
    [{ default_values: { 'k.k': 'v' } }, /cannot contain '\.'/],
    [{ default_values: { '': 'v' } }, /cannot be empty/],
  ])('rejects %j', (payload, msg) => {
    const res = StoreConfigModel.safeParse(payload as any);
    expect(res.success).toBe(false);
    expect(res.error!.issues.some(i => msg.test(i.message))).toBe(true);
  });
});

Also applies to: 110-116, 118-124, 126-132, 134-141

🤖 Prompt for AI Agents
In typescript-sdk/tests/test_models_and_statemanager_new.test.ts around lines
102 to 107 (and similarly for blocks at 110-116, 118-124, 126-132, 134-141),
multiple near-identical negative-path StoreConfigModel tests should be
consolidated into a single parameterized test using test.each; replace the
repeated it(...) blocks with a describe block that uses it.each([...cases])
where each case is [payload, expectedMessageRegex], call
StoreConfigModel.safeParse(payload as any), assert res.success is false, and
assert res.error!.issues.some(i => expectedMessageRegex.test(i.message)) to
verify the correct validation error; this deduplicates tests and makes adding
new invalid cases trivial.

Comment on lines +143 to +149
describe('RetryPolicyModel defaults', () => {
it('should have correct defaults', () => {
const pol = RetryPolicyModel.parse({});
expect(pol.max_retries).toBe(3);
expect(pol.backoff_factor).toBe(2000);
expect(pol.strategy).toBe(RetryStrategyEnum.EXPONENTIAL);
});
Copy link
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick

Cover all defaults: add exponent and max_delay expectations.

This documents the full default contract of RetryPolicyModel.

   it('should have correct defaults', () => {
     const pol = RetryPolicyModel.parse({});
     expect(pol.max_retries).toBe(3);
     expect(pol.backoff_factor).toBe(2000);
     expect(pol.strategy).toBe(RetryStrategyEnum.EXPONENTIAL);
+    expect(pol.exponent).toBe(2);
+    expect(pol.max_delay).toBeUndefined();
   });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
describe('RetryPolicyModel defaults', () => {
it('should have correct defaults', () => {
const pol = RetryPolicyModel.parse({});
expect(pol.max_retries).toBe(3);
expect(pol.backoff_factor).toBe(2000);
expect(pol.strategy).toBe(RetryStrategyEnum.EXPONENTIAL);
});
describe('RetryPolicyModel defaults', () => {
it('should have correct defaults', () => {
const pol = RetryPolicyModel.parse({});
expect(pol.max_retries).toBe(3);
expect(pol.backoff_factor).toBe(2000);
expect(pol.strategy).toBe(RetryStrategyEnum.EXPONENTIAL);
expect(pol.exponent).toBe(2);
expect(pol.max_delay).toBeUndefined();
});
🤖 Prompt for AI Agents
In typescript-sdk/tests/test_models_and_statemanager_new.test.ts around lines
143-149, the test checks only max_retries, backoff_factor, and strategy; add
assertions for exponent and max_delay to fully cover the model defaults — e.g.
after creating pol = RetryPolicyModel.parse({}) add
expect(pol.exponent).toBe(<default exponent value>) and
expect(pol.max_delay).toBe(<default max_delay value>) using the actual default
values defined on RetryPolicyModel (or import the constants) so the test
documents the complete default contract.

Comment on lines +153 to +175
it('should include store config in upsert', async () => {
// This test would require mocking the StateManager and its HTTP calls
// For now, we'll test the model parsing which is the core functionality
const storeCfg = StoreConfigModel.parse({
required_keys: ['k1'],
default_values: { 'k2': 'v' }
});

expect(storeCfg.required_keys).toEqual(['k1']);
expect(storeCfg.default_values).toEqual({ 'k2': 'v' });
});

it('should handle store in trigger', () => {
// This test would require mocking the StateManager and its HTTP calls
// For now, we'll test that the models can be parsed correctly
const storeConfig = StoreConfigModel.parse({
required_keys: ['cursor'],
default_values: { 'cursor': '0' }
});

expect(storeConfig.required_keys).toEqual(['cursor']);
expect(storeConfig.default_values).toEqual({ 'cursor': '0' });
});
Copy link
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick

Remove unnecessary async from non-async test.

No await is used; avoid async to prevent accidental swallowed rejections.

-  it('should include store config in upsert', async () => {
+  it('should include store config in upsert', () => {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
it('should include store config in upsert', async () => {
// This test would require mocking the StateManager and its HTTP calls
// For now, we'll test the model parsing which is the core functionality
const storeCfg = StoreConfigModel.parse({
required_keys: ['k1'],
default_values: { 'k2': 'v' }
});
expect(storeCfg.required_keys).toEqual(['k1']);
expect(storeCfg.default_values).toEqual({ 'k2': 'v' });
});
it('should handle store in trigger', () => {
// This test would require mocking the StateManager and its HTTP calls
// For now, we'll test that the models can be parsed correctly
const storeConfig = StoreConfigModel.parse({
required_keys: ['cursor'],
default_values: { 'cursor': '0' }
});
expect(storeConfig.required_keys).toEqual(['cursor']);
expect(storeConfig.default_values).toEqual({ 'cursor': '0' });
});
it('should include store config in upsert', () => {
// This test would require mocking the StateManager and its HTTP calls
// For now, we'll test the model parsing which is the core functionality
const storeCfg = StoreConfigModel.parse({
required_keys: ['k1'],
default_values: { 'k2': 'v' }
});
expect(storeCfg.required_keys).toEqual(['k1']);
expect(storeCfg.default_values).toEqual({ 'k2': 'v' });
});
it('should handle store in trigger', () => {
// This test would require mocking the StateManager and its HTTP calls
// For now, we'll test that the models can be parsed correctly
const storeConfig = StoreConfigModel.parse({
required_keys: ['cursor'],
default_values: { 'cursor': '0' }
});
expect(storeConfig.required_keys).toEqual(['cursor']);
expect(storeConfig.default_values).toEqual({ 'cursor': '0' });
});
🤖 Prompt for AI Agents
In typescript-sdk/tests/test_models_and_statemanager_new.test.ts around lines
153 to 175 the first test function is declared async but contains no await;
remove the unnecessary async keyword from the it('should include store config in
upsert', ...) declaration so the test is a regular synchronous function, then
run the test suite to ensure no behavioral changes.

nk-ag and others added 3 commits September 22, 2025 08:29
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
- Downgraded the SDK version from 0.1.0 to 0.0.1 in package.json and package-lock.json for proper versioning.
- Added license information and Node.js engine requirements in package-lock.json.
- Updated Vitest configuration to exclude pending tests.
- Refactored version script to use ES module syntax for better compatibility.
- Introduced a new integration test file for comprehensive testing of runtime and state manager functionalities.
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 5

📜 Review details

Configuration used: CodeRabbit UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 0cc1335 and b74621c.

⛔ Files ignored due to path filters (1)
  • typescript-sdk/package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (7)
  • .github/workflows/publish-typescript-sdk.yml (1 hunks)
  • typescript-sdk/exospherehost/types.ts (1 hunks)
  • typescript-sdk/package.json (1 hunks)
  • typescript-sdk/scripts/version.js (1 hunks)
  • typescript-sdk/tests/pending_test_integration.test.ts (1 hunks)
  • typescript-sdk/tests/test_signals_and_runtime_functions.test.ts (1 hunks)
  • typescript-sdk/vitest.config.ts (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
typescript-sdk/tests/test_signals_and_runtime_functions.test.ts (2)
typescript-sdk/exospherehost/signals.ts (2)
  • PruneSignal (1-20)
  • ReQueueAfterSignal (22-44)
typescript-sdk/exospherehost/runtime.ts (1)
  • Runtime (58-357)
typescript-sdk/tests/pending_test_integration.test.ts (1)
python-sdk/tests/test_integration.py (3)
  • IntegrationTestNode (42-59)
  • MultiOutputNode (62-74)
  • ErrorProneNode (77-90)
🪛 YAMLlint (1.37.1)
.github/workflows/publish-typescript-sdk.yml

[warning] 3-3: truthy value should be one of [false, true]

(truthy)

🔇 Additional comments (26)
typescript-sdk/package.json (7)

31-36: zod placement looks correct

Peer + dev for local builds is appropriate. Ensure consumers install a compatible zod.


2-2: Prefer a scoped package name

Use a scope to avoid collisions and signal ownership, e.g. "@exospherehost/sdk".

-  "name": "exospherehost",
+  "name": "@exospherehost/sdk",

15-16: Make clean cross‑platform

Use rimraf and declare it.

-    "clean": "rm -rf dist",
+    "clean": "rimraf dist",
   "devDependencies": {
     "@types/node": "^20.14.11",
     "@vitest/coverage-v8": "^1.6.0",
+    "rimraf": "^5.0.5",
     "typescript": "^5.6.3",
     "vitest": "^1.6.0",
     "zod": "^3.23.8"
   },

Also applies to: 26-32


33-36: Mark @types/node peer as optional

Reduces friction for consumers without ambient Node types.

   "peerDependencies": {
     "@types/node": ">=20.0.0",
     "zod": "^3.23.8"
   },
+  "peerDependenciesMeta": {
+    "@types/node": {
+      "optional": true
+    }
+  },

6-7: Add exports map and mark package side‑effect free

Prevents deep imports and improves tree‑shaking.

   "main": "dist/index.js",
   "types": "dist/index.d.ts",
+  "exports": {
+    ".": {
+      "types": "./dist/index.d.ts",
+      "import": "./dist/index.js"
+    }
+  },
+  "sideEffects": false,

63-65: Pin engines to Node 18.17+

Ensures stable, up‑to‑date WHATWG fetch/undici behavior.

   "engines": {
-    "node": ">=18.0.0"
+    "node": ">=18.17"
   },

8-22: Add typecheck and prepare scripts

Type safety in CI and auto‑builds when publishing from git.

   "scripts": {
     "build": "tsc",
+    "typecheck": "tsc -p tsconfig.json --noEmit",
     "test": "vitest",
@@
     "prepack": "npm run build",
+    "prepare": "npm run build",
typescript-sdk/exospherehost/types.ts (1)

6-12: Types look good; confirm JSON‑serializable inputs for GraphNode

If inputs must be JSON‑safe, consider a JSONValue alias; keep TriggerState.inputs as string as callers rely on it.

Also applies to: 14-20

typescript-sdk/scripts/version.js (3)

102-104: Avoid hardcoding branch in guidance

Use a generic hint.

-  console.log('3. Push to trigger publishing:');
-  console.log('   git push origin main');
+  console.log('3. Push to trigger publishing (adjust branch as needed):');
+  console.log('   git push origin <branch>');

119-129: Fix help text to valid prerelease format

Show -beta.N examples.

-  beta    Create a beta version (e.g., 0.1.0b1, 0.1.0b2)
+  beta    Create a beta version (e.g., 0.1.0-beta.1, 0.1.0-beta.2)
@@
-  node scripts/version.js beta    # 0.1.0 → 0.1.0b1
-  node scripts/version.js patch   # 0.1.0b1 → 0.1.0
+  node scripts/version.js beta    # 0.1.0 → 0.1.0-beta.1
+  node scripts/version.js patch   # 0.1.0-beta.1 → 0.1.0

28-110: Blocker: invalid prerelease scheme (b1) will fail npm publish; switch to semver -beta.N with strict parsing

Current split/contains logic emits non‑semver like 1.0.0b1. Replace with a semver‑aware flow.

 function updateVersion(type) {
   const currentVersion = getCurrentVersion();
   console.log(`Current version: ${currentVersion}`);
-  
-  let newVersion;
-  
-  switch (type) {
-    case 'beta':
-      // If current version already has beta, increment beta number
-      if (currentVersion.includes('b')) {
-        const [baseVersion, betaNum] = currentVersion.split('b');
-        newVersion = `${baseVersion}b${parseInt(betaNum) + 1}`;
-      } else {
-        // Create first beta version
-        newVersion = `${currentVersion}b1`;
-      }
-      break;
-      
-    case 'patch':
-      if (currentVersion.includes('b')) {
-        // Remove beta suffix for stable release
-        newVersion = currentVersion.split('b')[0];
-      } else {
-        // Increment patch version
-        const [major, minor, patch] = currentVersion.split('.').map(Number);
-        newVersion = `${major}.${minor}.${patch + 1}`;
-      }
-      break;
-      
-    case 'minor':
-      if (currentVersion.includes('b')) {
-        // Remove beta suffix and increment minor
-        const [major, minor] = currentVersion.split('b')[0].split('.').map(Number);
-        newVersion = `${major}.${minor + 1}.0`;
-      } else {
-        // Increment minor version
-        const [major, minor] = currentVersion.split('.').map(Number);
-        newVersion = `${major}.${minor + 1}.0`;
-      }
-      break;
-      
-    case 'major':
-      if (currentVersion.includes('b')) {
-        // Remove beta suffix and increment major
-        const major = parseInt(currentVersion.split('b')[0].split('.')[0]) + 1;
-        newVersion = `${major}.0.0`;
-      } else {
-        // Increment major version
-        const major = parseInt(currentVersion.split('.')[0]) + 1;
-        newVersion = `${major}.0.0`;
-      }
-      break;
-      
-    default:
-      console.error(`Unknown version type: ${type}`);
-      console.error('Valid types: beta, patch, minor, major');
-      process.exit(1);
-  }
-  
-  console.log(`New version: ${newVersion}`);
-  
-  // Update package.json
-  packageJson.version = newVersion;
+  
+  const SEMVER_RE = /^(\d+)\.(\d+)\.(\d+)(?:-beta\.(\d+))?$/;
+  const m = currentVersion.match(SEMVER_RE);
+  if (!m) {
+    console.error(`package.json version '${currentVersion}' is not supported. Expected X.Y.Z or X.Y.Z-beta.N`);
+    process.exit(1);
+  }
+  let major = +m[1], minor = +m[2], patch = +m[3];
+  let beta = m[4] ? +m[4] : null;
+
+  switch (type) {
+    case 'beta':
+      beta = beta != null ? beta + 1 : 1;
+      break;
+    case 'patch':
+      if (beta != null) {
+        beta = null; // finalize prerelease
+      } else {
+        patch += 1;
+      }
+      break;
+    case 'minor':
+      minor += 1; patch = 0; beta = null;
+      break;
+    case 'major':
+      major += 1; minor = 0; patch = 0; beta = null;
+      break;
+    default:
+      console.error(`Unknown version type: ${type}`);
+      console.error('Valid types: beta, patch, minor, major');
+      process.exit(1);
+  }
+
+  const newVersion = beta != null
+    ? `${major}.${minor}.${patch}-beta.${beta}`
+    : `${major}.${minor}.${patch}`;
+  console.log(`New version: ${newVersion}`);
+
+  packageJson.version = newVersion;
   fs.writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2) + '\n');
.github/workflows/publish-typescript-sdk.yml (3)

65-73: Gate publish job to push events to avoid double publish on release

Prevents duplicate npm publishes.

-    if: github.repository == 'exospherehost/exospherehost'
+    if: github.event_name == 'push' && github.repository == 'exospherehost/exospherehost'

91-101: Semver‑aware npm tag detection

Match -beta prereleases; avoid any “b”.

-      - name: Check version for beta indicator
+      - name: Determine npm tag from semver prerelease
         run: |
           VERSION=$(node -p "require('./package.json').version")
-          if [[ "$VERSION" == *"b"* ]]; then
-            echo "Version $VERSION contains beta indicator - publishing to npm with beta tag"
-            echo "NPM_TAG=beta" >> $GITHUB_ENV
-          else
-            echo "Version $VERSION does not contain beta indicator - publishing to npm with latest tag"
-            echo "NPM_TAG=latest" >> $GITHUB_ENV
-          fi
+          if [[ "$VERSION" == *"-beta."* ]]; then
+            echo "NPM_TAG=beta" >> "$GITHUB_ENV"
+          elif [[ "$VERSION" == *"-"* ]]; then
+            echo "NPM_TAG=next" >> "$GITHUB_ENV"
+          else
+            echo "NPM_TAG=latest" >> "$GITHUB_ENV"
+          fi

153-157: Remove duplicate publish in release job

Publishing already happens in publish job.

-      - name: Publish to npm with latest tag
-        run: npm publish --tag latest --access public
-        env:
-          NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
+      # Skip npm publish here; already handled in the 'publish' job on push.
typescript-sdk/vitest.config.ts (2)

1-4: Pin Vitest root (monorepo safety)

Prevents CWD surprises when run from repo root.

-import { defineConfig } from 'vitest/config';
+import { fileURLToPath } from 'node:url';
+import { defineConfig } from 'vitest/config';
+
+const root = fileURLToPath(new URL('.', import.meta.url));
 
-export default defineConfig({
+export default defineConfig({
+  root,

7-7: *Discover .spec.ts too

Covers common test suffix.

-    include: ['tests/**/*.test.ts'],
+    include: ['tests/**/*.{test,spec}.ts'],
typescript-sdk/tests/pending_test_integration.test.ts (2)

118-120: Remove incorrect nodeMapping overrides

nodeMapping is keyed by node class name; overriding with state_id is misleading and unnecessary.

-    (runtime as any).nodeMapping['test_state_1'] = IntegrationTestNode;
@@
-    (runtime as any).nodeMapping[state.state_id] = MultiOutputNode;
@@
-    (runtime as any).nodeMapping['test_state_1'] = ErrorProneNode;
@@
-    (runtime as any).nodeMapping[state.state_id] = IntegrationTestNode;
@@
-    states.forEach(state => {
-      (runtime as any).nodeMapping[state.state_id] = IntegrationTestNode;
-    });
+    // No overrides needed; runtime already maps by node class

Also applies to: 210-211, 243-244, 297-298, 401-405


128-131: Tests start endless workers; make the loop abortable or bound to one iteration

As written, worker() never resolves and can hang CI.

Suggested approaches:

  • Add an AbortSignal param to Runtime.worker and pass AbortController.signal in tests; abort after first assertion.
  • Or mock the poll loop (e.g., spy on get/enqueue to return once, then throw/return sentinel) and await worker exit.
  • Prefer vi.useFakeTimers to drive any polling timers deterministically, combined with an exit condition.

Also applies to: 218-221, 251-254, 304-308, 416-420

typescript-sdk/tests/test_signals_and_runtime_functions.test.ts (8)

28-31: Good: per-test mock reset present.

Clearing mocks in this suite prevents cross-test leakage.


233-251: LGTM: needSecrets coverage is precise.

Good positive/negative assertions for presence of secrets.


496-574: Good: notifyExecuted/notifyErrored behavior tested including failure paths.

Covers headers, body shape, and non-throwing failure handling.


72-81: Make failure assertion resilient to appended details.

Implementation appends the endpoint to the error; prefer regex match.

-    await expect(signal.send('http://test-endpoint/prune', 'test-api-key')).rejects.toThrow('Failed to send prune signal');
+    await expect(signal.send('http://test-endpoint/prune', 'test-api-key')).rejects.toThrow(/Failed to send prune signal/);

84-85: Reset mocks in ReQueueAfterSignal suite.

Add per-test vi.clearAllMocks() like other suites.

 describe('TestReQueueAfterSignal', () => {
+  beforeEach(() => {
+    vi.clearAllMocks();
+  });

139-148: Make failure assertion resilient to appended details.

Align with prune failure path; use regex.

-    await expect(signal.send('http://test-endpoint/requeue', 'test-api-key')).rejects.toThrow('Failed to send requeue after signal');
+    await expect(signal.send('http://test-endpoint/requeue', 'test-api-key')).rejects.toThrow(/Failed to send requeue after signal/);

89-92: Strengthen message assertions to validate format and delay interpolation.

Also assert the delay appears with "ms" and the full guidance phrase.

-    expect(signal.message).toContain('ReQueueAfter signal received with delay');
-    expect(signal.message).toContain('Do not catch this Exception');
+    expect(signal.message.startsWith('ReQueueAfter signal received with delay')).toBe(true);
+    expect(signal.message).toContain(`${delayMs}ms`);
+    expect(signal.message).toContain('Do not catch this Exception, let it bubble up to Runtime for handling at StateManager');

7-8: Centralize global.fetch mock in a shared test setup and clear mocks between tests.

  • Remove the inline mock from typescript-sdk/tests/test_signals_and_runtime_functions.test.ts (lines 7–8) and move it to tests/setup.ts; register that file via vitest config's setupFiles.
  • tests/setup.ts:
import { beforeEach, afterEach, vi } from 'vitest';
(globalThis as any).fetch = vi.fn();
beforeEach(() => vi.clearAllMocks());
afterEach(() => vi.clearAllMocks());
  • vitest.config.ts (test): { setupFiles: ['tests/setup.ts'], environment: 'node' }
  • Verify no other inline mocks remain: rg -n "global\.(fetch|this\.fetch)\s*=\s*vi\.fn\(" -g '!/node_modules/'
-// Mock fetch globally
-global.fetch = vi.fn();

Comment on lines +163 to +171
const graphNodes = [{
node_name: 'IntegrationTestNode',
namespace: 'test_namespace',
identifier: 'IntegrationTestNode',
inputs: { type: 'test' },
next_nodes: null,
unites: null
}];
const secrets = { api_key: 'test_key', database_url: 'db://test' };
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Fix graph node payload: avoid nulls for optional fields

Use undefined/omit instead of null to match schema; drop unknown fields.

-    const graphNodes = [{
+    const graphNodes = [{
       node_name: 'IntegrationTestNode',
       namespace: 'test_namespace',
       identifier: 'IntegrationTestNode',
-      inputs: { type: 'test' },
-      next_nodes: null,
-      unites: null
+      inputs: { type: 'test' }
     }];
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const graphNodes = [{
node_name: 'IntegrationTestNode',
namespace: 'test_namespace',
identifier: 'IntegrationTestNode',
inputs: { type: 'test' },
next_nodes: null,
unites: null
}];
const secrets = { api_key: 'test_key', database_url: 'db://test' };
const graphNodes = [{
node_name: 'IntegrationTestNode',
namespace: 'test_namespace',
identifier: 'IntegrationTestNode',
inputs: { type: 'test' }
}];
const secrets = { api_key: 'test_key', database_url: 'db://test' };
🤖 Prompt for AI Agents
In typescript-sdk/tests/pending_test_integration.test.ts around lines 163 to
171, the test graph node uses null for optional fields and includes fields not
in the schema; update the payload to omit optional fields (remove next_nodes and
unites or set them to undefined) rather than using null, and remove any unknown
fields (e.g., drop identifier if it isn't part of the schema) so the object
matches the expected schema shape.

@@ -0,0 +1,575 @@
import { describe, it, expect, beforeEach, vi } from 'vitest';
Copy link
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick

Import afterEach to support env teardown.

You add per-suite cleanup later; import afterEach here.

-import { describe, it, expect, beforeEach, vi } from 'vitest';
+import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
import { describe, it, expect, beforeEach, vi } from 'vitest';
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
🤖 Prompt for AI Agents
In typescript-sdk/tests/test_signals_and_runtime_functions.test.ts around line
1, the test file imports Vitest helpers but omits afterEach which is needed for
environment teardown; update the import statement to include afterEach (i.e.,
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest') so the
teardown code can run.

Comment on lines +119 to +137
it('should send with minutes', async () => {
(global.fetch as any).mockResolvedValueOnce({
ok: true
});

const delayMs = 150000; // 2.5 minutes
const signal = new ReQueueAfterSignal(delayMs);

await signal.send('http://test-endpoint/requeue', 'test-api-key');

expect(global.fetch).toHaveBeenCalledWith('http://test-endpoint/requeue', {
method: 'POST',
headers: {
'x-api-key': 'test-api-key',
'Content-Type': 'application/json'
},
body: JSON.stringify({ enqueue_after: delayMs })
});
});
Copy link
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick

Deduplicate “send successfully” tests via parametrization.

Combine these two nearly identical tests using test.each to cut duplication and simplify maintenance.

Example:

it.each([
  { delayMs: 45000 },
  { delayMs: 150000 }, // 2.5 minutes
])('should send successfully with delay $delayMs', async ({ delayMs }) => {
  (global.fetch as any).mockResolvedValueOnce({ ok: true });
  const signal = new ReQueueAfterSignal(delayMs);
  await signal.send('http://test-endpoint/requeue', 'test-api-key');
  expect(global.fetch).toHaveBeenCalledWith('http://test-endpoint/requeue', {
    method: 'POST',
    headers: { 'x-api-key': 'test-api-key', 'Content-Type': 'application/json' },
    body: JSON.stringify({ enqueue_after: delayMs }),
  });
});
🤖 Prompt for AI Agents
In typescript-sdk/tests/test_signals_and_runtime_functions.test.ts around lines
119 to 137, two nearly identical "send successfully" tests should be
deduplicated by replacing them with a single parameterized test using test.each;
create an array of test cases with the different delayMs values (e.g., 45000 and
150000), call (global.fetch as any).mockResolvedValueOnce({ ok: true }) inside
the test, instantiate ReQueueAfterSignal with the current delayMs, await
signal.send(...), and assert global.fetch was called with the same URL, method,
headers and body JSON.stringify({ enqueue_after: delayMs }); remove the
duplicate individual tests and keep the single test.each variant with a
descriptive name including the delay parameter.

Comment on lines +157 to +163
describe('TestRuntimeSignalHandling', () => {
beforeEach(() => {
vi.clearAllMocks();
process.env.EXOSPHERE_STATE_MANAGER_URI = 'http://test-state-manager';
process.env.EXOSPHERE_API_KEY = 'test-key';
});

Copy link
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick

Teardown env vars to avoid bleed across suites.

Remove test-specific ENV after each test in this suite.

   beforeEach(() => {
     vi.clearAllMocks();
     process.env.EXOSPHERE_STATE_MANAGER_URI = 'http://test-state-manager';
     process.env.EXOSPHERE_API_KEY = 'test-key';
   });
+  afterEach(() => {
+    delete process.env.EXOSPHERE_STATE_MANAGER_URI;
+    delete process.env.EXOSPHERE_API_KEY;
+  });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
describe('TestRuntimeSignalHandling', () => {
beforeEach(() => {
vi.clearAllMocks();
process.env.EXOSPHERE_STATE_MANAGER_URI = 'http://test-state-manager';
process.env.EXOSPHERE_API_KEY = 'test-key';
});
describe('TestRuntimeSignalHandling', () => {
beforeEach(() => {
vi.clearAllMocks();
process.env.EXOSPHERE_STATE_MANAGER_URI = 'http://test-state-manager';
process.env.EXOSPHERE_API_KEY = 'test-key';
});
afterEach(() => {
delete process.env.EXOSPHERE_STATE_MANAGER_URI;
delete process.env.EXOSPHERE_API_KEY;
});
🤖 Prompt for AI Agents
In typescript-sdk/tests/test_signals_and_runtime_functions.test.ts around lines
157-163, the beforeEach sets test-specific environment variables but there is no
teardown, risking bleed across suites; add an afterEach that restores or deletes
process.env.EXOSPHERE_STATE_MANAGER_URI and process.env.EXOSPHERE_API_KEY
(preferably capture originals in beforeEach and restore them, or at minimum
delete the two keys in afterEach) so tests do not leak environment state.

Comment on lines +449 to +472
it('should convert delay correctly to milliseconds', async () => {
const testCases = [
{ delayMs: 1000, expected: 1000 },
{ delayMs: 60000, expected: 60000 },
{ delayMs: 3600000, expected: 3600000 },
{ delayMs: 86400000, expected: 86400000 },
{ delayMs: 30500, expected: 30500 } // 30.5 seconds
];

for (const { delayMs, expected } of testCases) {
(global.fetch as any).mockResolvedValueOnce({ ok: true });

const signal = new ReQueueAfterSignal(delayMs);
await signal.send('http://test-endpoint', 'test-key');

expect(global.fetch).toHaveBeenCalledWith('http://test-endpoint', {
method: 'POST',
headers: {
'x-api-key': 'test-key',
'Content-Type': 'application/json'
},
body: JSON.stringify({ enqueue_after: expected })
});
}
Copy link
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick

Stabilize per-iteration assertions in the conversion loop.

Clear mocks at the start of each iteration (since you re-stub mockResolvedValueOnce) to ensure each expectation validates only the current call.

-    for (const { delayMs, expected } of testCases) {
-      (global.fetch as any).mockResolvedValueOnce({ ok: true });
+    for (const { delayMs, expected } of testCases) {
+      vi.clearAllMocks();
+      (global.fetch as any).mockResolvedValueOnce({ ok: true });

Alternatively, assert nth call order instead of clearing:

const callIndex = i + 1;
expect(global.fetch).toHaveBeenNthCalledWith(callIndex, 'http://test-endpoint', { /* ... */ });
🤖 Prompt for AI Agents
In typescript-sdk/tests/test_signals_and_runtime_functions.test.ts around lines
449 to 472, the loop re-stubs global.fetch with mockResolvedValueOnce but does
not clear or assert call index, which can make expectations flaky; either
clear/reset the mock at the start of each iteration (e.g., reset or mockClear)
so each expect checks only the current call, or compute the expected call index
and use toHaveBeenNthCalledWith for the specific iteration; update the test to
perform one of these two approaches so each assertion deterministically verifies
the intended fetch invocation.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants