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
42 changes: 42 additions & 0 deletions .changeset/service-storage-tests-pin-core-to-source.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
---
"@objectstack/service-storage": patch
---

test(service-storage): resolve `@objectstack/core` from source, so a stale dist can no longer decide a pin (#7668)

`packages/services/service-storage` had no `vitest.config.ts`, so its unit suite
resolved `@objectstack/core` through the workspace link to
`packages/core/dist/index.js` — a **build artifact**. The verdict of every unit
pin in the package was therefore a function of build state rather than of the
source in the checkout.

#7668 is what that costs. All 17 cases of `attachment-access-hooks.test.ts` —
the only executable guard on the #4757 predicate-less unscoped-multi-delete
refusal, which cannot be expressed over REST (`deleteMany` with no `ids`/`where`
is rejected with 400 before the hook is reached) — errored with
`TypeError: withoutOperationPrivateKeys is not a function` against a tree whose
prebuilt core predated that export. The source was correct throughout
(`packages/core/src/security/operation-private-keys.ts`), so #4757 was left
unguarded by anything runnable while nothing was actually broken.

The loud error is the mild half. A core dist that is merely **behind** rather
than missing the symbol lets a pin run **green** against core's old behaviour —
a passing test that is not testing the code in the checkout, with nothing in the
output saying so.

**Not a task-ordering bug.** `turbo.json` already declares `test` `dependsOn`
`^build`, and `pnpm turbo run test --filter=@objectstack/service-storage` builds
core first and passes 352/352; it needed no change. The paths that broke are the
ones turbo does not mediate — `pnpm test` inside the package, `vitest run <file>`,
an editor runner, or a QA agent in a tree built at an older commit — and those
are exactly the paths a pin is re-run on while someone is changing core, i.e.
when it most needs to be telling the truth. Ordering cannot fix that; taking the
artifact out of the resolution path can.

A `vitest.config.ts` now aliases `@objectstack/core` to `packages/core/src`,
matching what `service-knowledge`, `plugin-audit`, `runtime`, `metadata` and six
other packages already do. Aliasing is graph-wide, so the dependencies still
loaded from dist (`spec`, `observability`, `platform-objects`, `objectql`)
resolve to the same single core instance rather than a second copy; the shared
tsup config externalizes workspace deps, so none of them inline one. No product
code and no test assertions changed.
47 changes: 47 additions & 0 deletions packages/services/service-storage/vitest.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.

import { defineConfig } from 'vitest/config';
import path from 'path';

export default defineConfig({
test: {
environment: 'node',
},
resolve: {
// This package had no vitest config at all, so `@objectstack/core` resolved
// through the workspace link to `packages/core/dist/index.js` — a BUILD
// ARTIFACT. That made the verdict of every unit pin here a function of
// build state rather than of source, and #7668 is what that costs: all 17
// cases of `attachment-access-hooks.test.ts` — the only executable guard on
// the #4757 predicate-less unscoped-multi-delete refusal, which cannot be
// expressed over REST — errored with `TypeError:
// withoutOperationPrivateKeys is not a function` against a prebuilt tree
// whose core dist predated that export. The source was correct the whole
// time (`packages/core/src/security/operation-private-keys.ts`).
//
// The dangerous half is not the loud error. A dist that is merely BEHIND
// rather than missing the symbol lets a pin run GREEN against core's old
// behaviour — a passing test that is not testing the code in the checkout,
// and nothing in the output says so.
//
// Turbo already orders the build correctly (`test` dependsOn `^build`), so
// `turbo run test` was never the failing path and needed no change. Every
// OTHER way of running this suite was: `pnpm test` inside the package,
// `vitest run <file>`, an editor runner, or a QA agent in a tree built at
// an older commit. Those are exactly the paths a pin gets re-run on while
// someone is changing core — i.e. when it most needs to be telling the
// truth. Ordering cannot fix that; removing the artifact from the path can.
//
// Aliasing is graph-wide, so the packages still loaded from dist (spec,
// observability, platform-objects, objectql) resolve to this same single
// core instance rather than a second copy — the shared tsup config
// externalizes workspace deps, so none of them inline one.
//
// Array form with an anchored pattern, deliberately: the object form
// matches by PREFIX, so a bare `@objectstack/core` entry would also swallow
// `@objectstack/core/logger` and resolve it to `core/src/index.ts/logger`
// (ENOTDIR). Same reasoning, and same shape, as `service-knowledge`'s
// config.
alias: [{ find: /^@objectstack\/core$/, replacement: path.resolve(__dirname, '../../core/src/index.ts') }],
},
});
Loading