Skip to content

Commit 93be029

Browse files
huangyiireneclaude
andauthored
test(service-storage): resolve @objectstack/core from source so a stale dist can't decide a pin (#7668) (#7778)
`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 a function of build state, not of the source in the checkout. 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 tree whose prebuilt core predated that export, while `packages/core/src/security/operation-private-keys.ts` was correct throughout. The loud error is the mild half: a core dist merely BEHIND rather than missing the symbol lets a pin run green against core's old behaviour, with nothing in the output saying so. This is not a task-ordering bug. `turbo.json` already declares `test` dependsOn `^build` and `turbo run test --filter=@objectstack/service-storage` passes 352/352; it needed no change. The paths that broke are the ones turbo does not mediate — `pnpm test` in the package, `vitest run <file>`, an editor runner, a QA tree built at an older commit — which is where a pin is re-run while someone is changing core. Ordering cannot fix that; taking the artifact out of the resolution path can. Verified by simulating the exact #7668 condition (core's built `index.js` stripped of the export): without the config 17/30 cases fail with the issue's verbatim TypeError; with it, 30/30 pass. Full suite 352/352 green both via `turbo run test` and via a bare `vitest run` in the package. Fixes #7668 Claude-Session: https://claude.ai/code/session_01UqnHVpBA1ij5Jb87JaMXyM Co-authored-by: Claude <noreply@anthropic.com>
1 parent e906126 commit 93be029

2 files changed

Lines changed: 89 additions & 0 deletions

File tree

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
---
2+
"@objectstack/service-storage": patch
3+
---
4+
5+
test(service-storage): resolve `@objectstack/core` from source, so a stale dist can no longer decide a pin (#7668)
6+
7+
`packages/services/service-storage` had no `vitest.config.ts`, so its unit suite
8+
resolved `@objectstack/core` through the workspace link to
9+
`packages/core/dist/index.js` — a **build artifact**. The verdict of every unit
10+
pin in the package was therefore a function of build state rather than of the
11+
source in the checkout.
12+
13+
#7668 is what that costs. All 17 cases of `attachment-access-hooks.test.ts` —
14+
the only executable guard on the #4757 predicate-less unscoped-multi-delete
15+
refusal, which cannot be expressed over REST (`deleteMany` with no `ids`/`where`
16+
is rejected with 400 before the hook is reached) — errored with
17+
`TypeError: withoutOperationPrivateKeys is not a function` against a tree whose
18+
prebuilt core predated that export. The source was correct throughout
19+
(`packages/core/src/security/operation-private-keys.ts`), so #4757 was left
20+
unguarded by anything runnable while nothing was actually broken.
21+
22+
The loud error is the mild half. A core dist that is merely **behind** rather
23+
than missing the symbol lets a pin run **green** against core's old behaviour —
24+
a passing test that is not testing the code in the checkout, with nothing in the
25+
output saying so.
26+
27+
**Not a task-ordering bug.** `turbo.json` already declares `test` `dependsOn`
28+
`^build`, and `pnpm turbo run test --filter=@objectstack/service-storage` builds
29+
core first and passes 352/352; it needed no change. The paths that broke are the
30+
ones turbo does not mediate — `pnpm test` inside the package, `vitest run <file>`,
31+
an editor runner, or a QA agent in a tree built at an older commit — and those
32+
are exactly the paths a pin is re-run on while someone is changing core, i.e.
33+
when it most needs to be telling the truth. Ordering cannot fix that; taking the
34+
artifact out of the resolution path can.
35+
36+
A `vitest.config.ts` now aliases `@objectstack/core` to `packages/core/src`,
37+
matching what `service-knowledge`, `plugin-audit`, `runtime`, `metadata` and six
38+
other packages already do. Aliasing is graph-wide, so the dependencies still
39+
loaded from dist (`spec`, `observability`, `platform-objects`, `objectql`)
40+
resolve to the same single core instance rather than a second copy; the shared
41+
tsup config externalizes workspace deps, so none of them inline one. No product
42+
code and no test assertions changed.
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
2+
3+
import { defineConfig } from 'vitest/config';
4+
import path from 'path';
5+
6+
export default defineConfig({
7+
test: {
8+
environment: 'node',
9+
},
10+
resolve: {
11+
// This package had no vitest config at all, so `@objectstack/core` resolved
12+
// through the workspace link to `packages/core/dist/index.js` — a BUILD
13+
// ARTIFACT. That made the verdict of every unit pin here a function of
14+
// build state rather than of source, and #7668 is what that costs: all 17
15+
// cases of `attachment-access-hooks.test.ts` — the only executable guard on
16+
// the #4757 predicate-less unscoped-multi-delete refusal, which cannot be
17+
// expressed over REST — errored with `TypeError:
18+
// withoutOperationPrivateKeys is not a function` against a prebuilt tree
19+
// whose core dist predated that export. The source was correct the whole
20+
// time (`packages/core/src/security/operation-private-keys.ts`).
21+
//
22+
// The dangerous half is not the loud error. A dist that is merely BEHIND
23+
// rather than missing the symbol lets a pin run GREEN against core's old
24+
// behaviour — a passing test that is not testing the code in the checkout,
25+
// and nothing in the output says so.
26+
//
27+
// Turbo already orders the build correctly (`test` dependsOn `^build`), so
28+
// `turbo run test` was never the failing path and needed no change. Every
29+
// OTHER way of running this suite was: `pnpm test` inside the package,
30+
// `vitest run <file>`, an editor runner, or a QA agent in a tree built at
31+
// an older commit. Those are exactly the paths a pin gets re-run on while
32+
// someone is changing core — i.e. when it most needs to be telling the
33+
// truth. Ordering cannot fix that; removing the artifact from the path can.
34+
//
35+
// Aliasing is graph-wide, so the packages still loaded from dist (spec,
36+
// observability, platform-objects, objectql) resolve to this same single
37+
// core instance rather than a second copy — the shared tsup config
38+
// externalizes workspace deps, so none of them inline one.
39+
//
40+
// Array form with an anchored pattern, deliberately: the object form
41+
// matches by PREFIX, so a bare `@objectstack/core` entry would also swallow
42+
// `@objectstack/core/logger` and resolve it to `core/src/index.ts/logger`
43+
// (ENOTDIR). Same reasoning, and same shape, as `service-knowledge`'s
44+
// config.
45+
alias: [{ find: /^@objectstack\/core$/, replacement: path.resolve(__dirname, '../../core/src/index.ts') }],
46+
},
47+
});

0 commit comments

Comments
 (0)