Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion packages/adapters/hono/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@
"scripts": {
"build": "tsup --config ../../../tsup.config.ts && node ../../../scripts/check-dts-emitted.mjs",
"test": "vitest run",
"test:watch": "vitest"
"test:watch": "vitest",
"typecheck": "tsc --noEmit && tsc --noEmit -p tsconfig.test.json"
},
"dependencies": {
"@objectstack/plugin-hono-server": "workspace:*",
Expand Down
10 changes: 5 additions & 5 deletions packages/adapters/hono/src/__mocks__/runtime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,13 @@
// in `packages/**`. Declaring MORE than this adapter calls is fine (it calls
// only `getDiscoveryInfo`, `handleAuth` and `dispatch`); declaring something the
// dispatcher does not implement is not.
import { vi } from 'vitest';
import { vi, type Mock } from 'vitest';

export class HttpDispatcher {
getDiscoveryInfo = vi.fn().mockReturnValue({ version: '1.0', routes: {} });
handleAuth = vi.fn().mockResolvedValue({ handled: true, response: { status: 200, body: { ok: true } } });
handleMetadata = vi.fn().mockResolvedValue({ handled: true, response: { status: 200, body: { objects: [] } } });
handleData = vi.fn().mockResolvedValue({ handled: true, response: { status: 200, body: { records: [] } } });
getDiscoveryInfo: Mock = vi.fn().mockReturnValue({ version: '1.0', routes: {} });
handleAuth: Mock = vi.fn().mockResolvedValue({ handled: true, response: { status: 200, body: { ok: true } } });
handleMetadata: Mock = vi.fn().mockResolvedValue({ handled: true, response: { status: 200, body: { objects: [] } } });
handleData: Mock = vi.fn().mockResolvedValue({ handled: true, response: { status: 200, body: { records: [] } } });

constructor(_kernel: any) {}
}
Expand Down
9 changes: 7 additions & 2 deletions packages/adapters/hono/src/hono.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,18 @@ import { objectStackMiddleware, createHonoApp } from './index';

const mockKernel = { name: 'test-kernel' } as any;

// What `objectStackMiddleware` puts on the context (`c.set('objectStack', kernel)`).
// An app a test reads it back from must declare that variable: a bare `new Hono()`
// has no `Variables`, so Hono types `c.get` as taking `never`.
type KernelEnv = { Variables: { objectStack: { name: string } } };

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

it('sets kernel on context via c.set', async () => {
const app = new Hono();
const app = new Hono<KernelEnv>();
const middleware = objectStackMiddleware(mockKernel);

app.use('*', middleware);
Expand Down Expand Up @@ -61,7 +66,7 @@ describe('objectStackMiddleware', () => {
});

it('provides the correct kernel instance', async () => {
const app = new Hono();
const app = new Hono<KernelEnv>();
const middleware = objectStackMiddleware(mockKernel);

app.use('*', middleware);
Expand Down
75 changes: 75 additions & 0 deletions packages/adapters/hono/tsconfig.test.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
// The TEST-layer type-check program (#20800 -- the last three packages of the
// #4311 ledger, graduated by the #15048 / #15050 road: a sibling program named
// by the `typecheck` script). `tsconfig.json` beside this one stays exactly as
// it is: it is the BUILD config. `package.json`'s `typecheck` script NAMES this
// sibling (`tsc --noEmit -p tsconfig.test.json`), because a config no script
// invokes reads as coverage and delivers none.
//
// WHAT THE BUILD CONFIG ALREADY DID. `tsconfig.json` includes `src/**/*` with no
// test exclusion, so the program that reads these tests always existed; the
// package simply declared no `typecheck` script, so nothing ever ran it. That
// is the `service-automation` / `service-cluster` shape, not the
// exclude-and-compensate one, and AGENTS.md forbids ADDING such an exclusion.
//
// What differs from the build config, and what deliberately does NOT:
// - MODULE SEMANTICS and `lib`, nothing else. vitest runs these files as ESM
// through esbuild on a Node that has the ES2022 builtins. `lib` keeps the
// build config's `DOM` / `DOM.Iterable` (a child's `lib` REPLACES the
// parent's, it does not merge) and adds ES2022 for the Node the suite runs
// on. The build leg of the script (`tsc --noEmit`) keeps the NodeNext
// reading of the same files.
// - STRICTNESS IS UNTOUCHED. `strict`, `noUnusedLocals`, `noUnusedParameters`,
// `noImplicitReturns`, `noFallthroughCasesInSwitch`, `rootDir`, `paths` and
// `types` are all INHERITED and none is re-declared. ⚠️ A child that declared
// its own `paths` would REPLACE the parent map, so this file declares none.
// Nothing here may loosen a type rule; if a test does not compile, that is
// the finding.
//
// MEASURED at f8178ffece (origin/main), the workspace closure built first
// (`turbo run build --filter='@objectstack/hono^...'`), BEFORE any fix, once per
// config (`tsc --noEmit --pretty false [--listFiles] -p <config>`):
//
// program under this config 858 files (7 of them this package's `src`),
// all 5 of its `src/**/*.test.ts` in it
// BUILD config (tsconfig.json) 3 = TS2769 x2 + TS18046 x1
// THIS config 3 = TS2769 x2 + TS18046 x1
//
// The two readings AGREE, so no config-tier pile; the 3 were code tier. All in
// `hono.test.ts` (36, 69, 70): a bare `new Hono()` has Hono's default `Env`, which
// declares no `Variables`, so `c.get('objectStack')` is typed as taking `never`
// (TS2769 x2) and the `kernel` it returns is `unknown` (TS18046). The middleware
// under test does `c.set('objectStack', kernel)`, so the two tests that read the
// variable back now build their app as `new Hono<KernelEnv>()` with the variable
// declared (`{ objectStack: { name: string } }`, the one field the test reads).
// `objectStackMiddleware`'s `(c: any, next: any)` source signature was NOT touched.
//
// THESE 3 WERE NOT THE WHOLE PILE. With them fixed, THIS config reported 4 more:
// TS2883 ("The inferred type of 'getDiscoveryInfo' cannot be named without a
// reference to 'Procedure' from '.pnpm/@vitest+spy@4.1.11/...'") on the four
// `vi.fn()` class fields of `src/__mocks__/runtime.ts`. tsc withholds
// declaration-emit diagnostics while any semantic error stands, so the first
// reading could not show them, and the BUILD config never shows them at all
// (observed: exit 0 on the same file under NodeNext resolution; only this
// config's `bundler` resolution reports it). The `DEBT` ledger's "3" was a count
// of what the first config could see, not of what this layer holds. Repaired
// at the cause, the same way as the others: the four fields are annotated
// `Mock` (vitest's own mock type) instead of leaving the inferred type to name an
// unexported pnpm-store path. No cast, no `@ts-expect-error`, no `src` change:
// `src/__mocks__/runtime.ts` is a vitest alias target (`vitest.config.ts`), never
// built or published (`tsup` enters `src/index.ts` alone).
//
// AFTER: 0 and 0. There is NO `test-typecheck-debt.json` beside this config, and
// its ABSENCE is the zero: any error in any file here is red immediately, with no
// ledger to be added to -- the same call `metadata-fs` and
// `trigger-record-change` recorded.
{
"extends": "./tsconfig.json",
"compilerOptions": {
"noEmit": true,
"module": "esnext",
"moduleResolution": "bundler",
"lib": ["ES2022", "DOM", "DOM.Iterable"]
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}
3 changes: 2 additions & 1 deletion packages/cloud-connection/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@
},
"scripts": {
"build": "tsup --config ../../tsup.config.ts && node ../../scripts/check-dts-emitted.mjs",
"test": "vitest run"
"test": "vitest run",
"typecheck": "tsc --noEmit && tsc --noEmit -p tsconfig.test.json"
},
"dependencies": {
"@objectstack/core": "workspace:*",
Expand Down
6 changes: 3 additions & 3 deletions packages/cloud-connection/src/cloud-connection-plugin.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,7 @@ describe('multi-tenant mode (env-registry + per-env kernel auth)', () => {
it('install forwards to the control plane install action for an authenticated session', async () => {
const rawApp = makeRawApp();
const { ctx, fireKernelReady } = makeCtx({ rawApp, services: mtServices('user-1') });
const fetchSpy = vi.fn(async () => new Response(JSON.stringify({ success: true, data: { installed: true } }), { status: 200 }));
const fetchSpy = vi.fn(async (_url: string, _init?: RequestInit) => new Response(JSON.stringify({ success: true, data: { installed: true } }), { status: 200 }));
vi.stubGlobal('fetch', fetchSpy);
await new CloudConnectionPlugin({ controlPlaneUrl: 'http://cloud.test', controlPlaneApiKey: 'svc-key' }).start(ctx as any);
await fireKernelReady();
Expand Down Expand Up @@ -180,7 +180,7 @@ describe('multi-tenant mode (env-registry + per-env kernel auth)', () => {
const rawApp = makeRawApp();
const { ctx, fireKernelReady } = makeCtx({ rawApp, services: mtServices('user-1') });
const items = [{ id: 'pkg_a', manifest_id: 'com.acme.crm' }];
const fetchSpy = vi.fn(async () => new Response(JSON.stringify({ success: true, data: { items } }), { status: 200 }));
const fetchSpy = vi.fn(async (_url: string, _init?: RequestInit) => new Response(JSON.stringify({ success: true, data: { items } }), { status: 200 }));
vi.stubGlobal('fetch', fetchSpy);
await new CloudConnectionPlugin({ controlPlaneUrl: 'http://cloud.test', controlPlaneApiKey: 'svc-key' }).start(ctx as any);
await fireKernelReady();
Expand All @@ -206,7 +206,7 @@ describe('single-environment mode (host auth, fixed env id)', () => {
it('uses the configured environment id + the host kernel auth session', async () => {
const rawApp = makeRawApp();
const { ctx, fireKernelReady } = makeCtx({ rawApp, services: { auth: sessionAuth('admin-1') } });
const fetchSpy = vi.fn(async () => new Response(JSON.stringify({ success: true, data: { packages: [{ id: 'p1' }] } }), { status: 200 }));
const fetchSpy = vi.fn(async (_url: string, _init?: RequestInit) => new Response(JSON.stringify({ success: true, data: { packages: [{ id: 'p1' }] } }), { status: 200 }));
vi.stubGlobal('fetch', fetchSpy);
await new CloudConnectionPlugin({
singleEnvironment: true,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ describe('CloudConnectionPlugin credential behavior', () => {
new ConnectionCredentialStore(credPath).write({ runtimeToken: 'oscc_stored', environmentId: 'env_1' });
const rawApp = makeRawApp();
const { ctx, fire } = makeCtx(rawApp, { auth: sessionAuth('admin'), manifest: { register: vi.fn() } });
const fetchSpy = vi.fn(async () => new Response(JSON.stringify({ success: true, data: { items: [] } }), { status: 200 }));
const fetchSpy = vi.fn(async (_url: string, _init?: RequestInit) => new Response(JSON.stringify({ success: true, data: { items: [] } }), { status: 200 }));
vi.stubGlobal('fetch', fetchSpy);
await new CloudConnectionPlugin({
singleEnvironment: true, controlPlaneUrl: 'http://cloud.test', controlPlaneApiKey: '', credentialPath: credPath,
Expand All @@ -134,7 +134,7 @@ describe('CloudConnectionPlugin credential behavior', () => {
store.write({ runtimeToken: 'oscc_stored', environmentId: 'env_1' });
const rawApp = makeRawApp();
const { ctx, fire } = makeCtx(rawApp, { auth: sessionAuth('admin'), manifest: { register: vi.fn() } });
const fetchSpy = vi.fn(async () => new Response(JSON.stringify({ success: true }), { status: 200 }));
const fetchSpy = vi.fn(async (_url: string, _init?: RequestInit) => new Response(JSON.stringify({ success: true }), { status: 200 }));
vi.stubGlobal('fetch', fetchSpy);
await new CloudConnectionPlugin({
singleEnvironment: true, controlPlaneUrl: 'http://cloud.test', controlPlaneApiKey: '', credentialPath: credPath,
Expand Down Expand Up @@ -221,7 +221,7 @@ describe('CloudConnectionPlugin credential behavior', () => {
store.write({ runtimeToken: 'oscc_stored', runtimeId: 'rt-1' });
const rawApp = makeRawApp();
const { ctx, fire } = makeCtx(rawApp, { auth: sessionAuth('admin'), manifest: { register: vi.fn() } });
const fetchSpy = vi.fn(async () => new Response(JSON.stringify({ success: true }), { status: 200 }));
const fetchSpy = vi.fn(async (_url: string, _init?: RequestInit) => new Response(JSON.stringify({ success: true }), { status: 200 }));
vi.stubGlobal('fetch', fetchSpy);
await new CloudConnectionPlugin({
singleEnvironment: true, controlPlaneUrl: 'http://cloud.test', controlPlaneApiKey: '', credentialPath: credPath,
Expand Down Expand Up @@ -267,7 +267,7 @@ describe('CloudConnectionPlugin credential behavior', () => {
new ConnectionCredentialStore(credPath).write({ runtimeToken: 'oscc_stored', runtimeId: 'rt-1' });
const rawApp = makeRawApp();
const { ctx, fire } = makeCtx(rawApp, { auth: sessionAuth('admin'), manifest: { register: vi.fn() } });
const fetchSpy = vi.fn(async () => new Response(JSON.stringify({ success: true, data: { items: [{ id: 'p1' }] } }), { status: 200 }));
const fetchSpy = vi.fn(async (_url: string, _init?: RequestInit) => new Response(JSON.stringify({ success: true, data: { items: [{ id: 'p1' }] } }), { status: 200 }));
vi.stubGlobal('fetch', fetchSpy);
await new CloudConnectionPlugin({
singleEnvironment: true, controlPlaneUrl: 'http://cloud.test', controlPlaneApiKey: '', credentialPath: credPath,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ describe('install-local compiled-bundle normalization', () => {

// The UI bundle registers at kernel:ready; the LAST register call is
// the installed package — flattened: top-level id + sections.
const installed = register.mock.calls.at(-1)![0];
const installed = register.mock.lastCall![0];
expect(installed.id).toBe('app.acme.crm');
expect(installed.namespace).toBe('crm');
expect(installed.version).toBe('2.0.0');
Expand All @@ -103,6 +103,6 @@ describe('install-local compiled-bundle normalization', () => {
makeC({ manifest: flat }),
);
expect(res.payload?.success).toBe(true);
expect(register.mock.calls.at(-1)![0].id).toBe('com.acme.flat');
expect(register.mock.lastCall![0].id).toBe('com.acme.flat');
});
});
72 changes: 72 additions & 0 deletions packages/cloud-connection/tsconfig.test.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
// The TEST-layer type-check program (#20800 -- the last three packages of the
// #4311 ledger, graduated by the #15048 / #15050 road: a sibling program named
// by the `typecheck` script). `tsconfig.json` beside this one stays exactly as
// it is: it is the BUILD config. `package.json`'s `typecheck` script NAMES this
// sibling (`tsc --noEmit -p tsconfig.test.json`), because a config no script
// invokes reads as coverage and delivers none.
//
// WHAT THE BUILD CONFIG ALREADY DID. `tsconfig.json` includes `src` with no
// test exclusion, so the program that reads these tests always existed; the
// package simply declared no `typecheck` script, so nothing ever ran it. That
// is the `service-automation` / `service-cluster` shape, not the
// exclude-and-compensate one, and AGENTS.md forbids ADDING such an exclusion.
//
// What differs from the build config, and what deliberately does NOT:
// - MODULE SEMANTICS and `lib`, nothing else. vitest runs these files as ESM
// through esbuild on a Node that has the ES2022 builtins, and the root
// config's `lib` is ES2020. `module: esnext` / `moduleResolution: bundler`
// is the reading vitest really performs; the build leg of the script
// (`tsc --noEmit`) keeps the stricter NodeNext reading of the same files,
// so the two programs complement each other rather than overlap.
// - STRICTNESS IS UNTOUCHED. `strict`, `noUnusedLocals`, `noUnusedParameters`,
// `noImplicitReturns`, `noFallthroughCasesInSwitch`, `rootDir`, `paths` and
// `types` are all INHERITED and none is re-declared. ⚠️ A child that declared
// its own `paths` would REPLACE the parent map, so this file declares none.
// Nothing here may loosen a type rule; if a test does not compile, that is
// the finding.
//
// MEASURED at f8178ffece (origin/main), the workspace closure built first
// (`turbo run build --filter='@objectstack/cloud-connection^...'`), BEFORE any
// fix, once per config (`tsc --noEmit --pretty false [--listFiles] -p <config>`):
//
// program under this config 923 files (44 of them this package's `src`),
// all 30 of its `src/**/*.test.ts` in it
// BUILD config (tsconfig.json) 13 = TS2493 x11 + TS2550 x2
// THIS config 11 = TS2493 x11
//
// The two readings DISAGREE by exactly two, and those two are the config tier:
// TS2550 ("Property 'at' does not exist ... change your target library"), both
// `Array#at` in `marketplace-install-local-bundle.test.ts` (81, 106) -- the CHECK
// reading ES2020 where the runtime is Node >= 22, which is what `lib` below
// reconciles. The other 11 are code tier and agree under both configs: TS2493
// ("Tuple type '[]' of length '0' has no element at index N"), every one an
// index into `fetchSpy.mock.calls[0]` where `fetchSpy = vi.fn(async () => ...)`
// was declared with NO parameters, so vitest types `.mock.calls` as an array of
// EMPTY tuples. The call site it stubs is `fetch(url, init)`, which the plugin
// always calls with a string URL and an init object; the fix types each of the 7
// declarations (`cloud-connection-plugin.test.ts` x3,
// `connection-credential-store.test.ts` x4) to that call --
// `vi.fn(async (_url: string, _init?: RequestInit) => ...)`. No cast was added,
// no `@ts-expect-error`, no `src` signature touched.
//
// The build leg reads these tests under ES2020 too, so the two `Array#at` reads
// were not left to this file's `lib`: they now use vitest's own
// `mock.lastCall` (the LAST register call, which is what both comments say),
// which compiles under both programs. Without that the first leg of `typecheck`
// would stay red on files this leg accepts.
//
// AFTER: 0 and 0. There is NO `test-typecheck-debt.json` beside this config, and
// its ABSENCE is the zero: any error in any file here is red immediately, with no
// ledger to be added to -- the same call `metadata-fs` and
// `trigger-record-change` recorded.
{
"extends": "./tsconfig.json",
"compilerOptions": {
"noEmit": true,
"module": "esnext",
"moduleResolution": "bundler",
"lib": ["ES2022"]
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}
3 changes: 2 additions & 1 deletion packages/observability/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@
},
"scripts": {
"build": "tsup --config ../../tsup.config.ts && node ../../scripts/check-dts-emitted.mjs",
"test": "vitest run"
"test": "vitest run",
"typecheck": "tsc --noEmit && tsc --noEmit -p tsconfig.test.json"
},
"dependencies": {
"@objectstack/spec": "workspace:*"
Expand Down
3 changes: 2 additions & 1 deletion packages/observability/src/__tests__/error-exporters.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.

import { describe, it, expect } from 'vitest';
import type { ErrorReporter } from '../contracts.js';
import {
NoopErrorReporter,
InMemoryErrorReporter,
Expand All @@ -9,7 +10,7 @@ import {

describe('NoopErrorReporter', () => {
it('captures without throwing or recording anything', () => {
const r = new NoopErrorReporter();
const r: ErrorReporter = new NoopErrorReporter();
expect(() => r.captureException(new Error('boom'), { x: 1 })).not.toThrow();
});
});
Expand Down
5 changes: 3 additions & 2 deletions packages/observability/src/__tests__/loggers.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.

import { describe, it, expect } from 'vitest';
import type { Logger } from '../contracts.js';
import { NoopLogger, ConsoleLogger, JsonLogger } from '../loggers.js';

function makeSink() {
Expand All @@ -15,7 +16,7 @@ function makeSink() {

describe('NoopLogger', () => {
it('discards every call', () => {
const l = new NoopLogger();
const l: Logger = new NoopLogger();
expect(() => {
l.debug('x');
l.info('x');
Expand All @@ -26,7 +27,7 @@ describe('NoopLogger', () => {
});

it('child() returns a logger that also discards', () => {
const l = new NoopLogger();
const l: Logger = new NoopLogger();
const c = l.child?.({ foo: 'bar' });
expect(c).toBeDefined();
expect(() => c?.info('hi')).not.toThrow();
Expand Down
Loading
Loading