|
| 1 | +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. |
| 2 | +// |
| 3 | +// [#13366] The default environment id a standalone boot stamps, pinned at the |
| 4 | +// place it is OBSERVABLE: the two plugins `createStandaloneStack` hands it to. |
| 5 | +// |
| 6 | +// Why this file exists at all. The v5.0 `project` to `environment` rename |
| 7 | +// shipped the CLI default `env_local` — `packages/cli/CHANGELOG.md` records |
| 8 | +// "Default local env id: `proj_local` -> `env_local`" and |
| 9 | +// `content/docs/deployment/cli.mdx` documents `env_local` — but the runtime's |
| 10 | +// own fallback kept stamping `proj_local`. Nothing pinned it, in either |
| 11 | +// spelling, so `declared != enforced` held on a published default for a whole |
| 12 | +// major line without one test going red. That is the gap this closes: the |
| 13 | +// literal now has an assertion attached to the code path that emits it. |
| 14 | +// |
| 15 | +// It reads the id off `result.plugins` rather than off a copy of the constant, |
| 16 | +// because the value is only interesting where it LANDS. `MetadataPlugin` takes |
| 17 | +// it as `options.environmentId` and `ObjectQLPlugin` as a row-scope key; a |
| 18 | +// pin that re-declared the string would stay green through a change that |
| 19 | +// stopped passing it to either. |
| 20 | +// |
| 21 | +// ⛔ These cases must NOT be read as "the CLI default". `os dev` / `os start` |
| 22 | +// export `OS_ENVIRONMENT_ID` into the child boot, so a CLI-spawned kernel never |
| 23 | +// reaches this fallback — the CLI's own default is pinned separately (the |
| 24 | +// `runtime.env_local.json` publication tests in packages/cli). What this file |
| 25 | +// owns is the DIRECT-EMBEDDER path: `createStandaloneStack()` with no config |
| 26 | +// and no env var, which is the surface a `createStandaloneStack` host observes. |
| 27 | + |
| 28 | +import { describe, it, expect, beforeEach, afterEach } from 'vitest'; |
| 29 | +import { mkdtempSync, rmSync } from 'node:fs'; |
| 30 | +import { tmpdir } from 'node:os'; |
| 31 | +import { join } from 'node:path'; |
| 32 | +import { createStandaloneStack } from './standalone-stack.js'; |
| 33 | + |
| 34 | +const BOOT_TIMEOUT = 60_000; |
| 35 | + |
| 36 | +// The two plugin ids the stack composes. Matched by the plugin's own declared |
| 37 | +// `name`, not by array position: the composition order is documented as a |
| 38 | +// dependency-graph outcome elsewhere in this package, and an index would pin |
| 39 | +// that instead of this. |
| 40 | +const METADATA_PLUGIN = 'com.objectstack.metadata'; |
| 41 | +const OBJECTQL_PLUGIN = 'com.objectstack.engine.objectql'; |
| 42 | + |
| 43 | +/** |
| 44 | + * The id as each plugin actually received it. |
| 45 | + * |
| 46 | + * `MetadataPlugin` keeps it under `options.environmentId`; `ObjectQLPlugin` |
| 47 | + * copies it to its own `environmentId` field. Both are TypeScript-private — |
| 48 | + * hence the casts — and reading them is deliberate: they are the last point at |
| 49 | + * which the stamped value is still identifiable before it dissolves into row |
| 50 | + * scoping and an artifact-validation envelope. |
| 51 | + */ |
| 52 | +function stampedIds(plugins: any[]): { metadata: unknown; objectql: unknown } { |
| 53 | + const metadata = plugins.find((p) => p?.name === METADATA_PLUGIN); |
| 54 | + const objectql = plugins.find((p) => p?.name === OBJECTQL_PLUGIN); |
| 55 | + expect(metadata, `stack must carry ${METADATA_PLUGIN}`).toBeDefined(); |
| 56 | + expect(objectql, `stack must carry ${OBJECTQL_PLUGIN}`).toBeDefined(); |
| 57 | + return { |
| 58 | + metadata: (metadata as any).options?.environmentId, |
| 59 | + objectql: (objectql as any).environmentId, |
| 60 | + }; |
| 61 | +} |
| 62 | + |
| 63 | +describe('[#13366] createStandaloneStack — default environment id', () => { |
| 64 | + let dir: string; |
| 65 | + let savedEnvId: string | undefined; |
| 66 | + let savedHome: string | undefined; |
| 67 | + |
| 68 | + beforeEach(() => { |
| 69 | + dir = mkdtempSync(join(tmpdir(), 'os-standalone-envid-')); |
| 70 | + savedEnvId = process.env.OS_ENVIRONMENT_ID; |
| 71 | + savedHome = process.env.OS_HOME; |
| 72 | + delete process.env.OS_ENVIRONMENT_ID; |
| 73 | + process.env.OS_HOME = dir; |
| 74 | + }); |
| 75 | + |
| 76 | + afterEach(() => { |
| 77 | + if (savedEnvId === undefined) delete process.env.OS_ENVIRONMENT_ID; |
| 78 | + else process.env.OS_ENVIRONMENT_ID = savedEnvId; |
| 79 | + if (savedHome === undefined) delete process.env.OS_HOME; |
| 80 | + else process.env.OS_HOME = savedHome; |
| 81 | + try { rmSync(dir, { recursive: true, force: true }); } catch { /* noop */ } |
| 82 | + }); |
| 83 | + |
| 84 | + it('stamps `env_local` when neither the config nor OS_ENVIRONMENT_ID names one', async () => { |
| 85 | + const stack = await createStandaloneStack({ databaseUrl: 'memory://standalone-envid-default' }); |
| 86 | + // The literal, at both landing sites. `proj_local` here is the pre-#13366 |
| 87 | + // value and is what this case exists to keep from coming back. |
| 88 | + expect(stampedIds(stack.plugins)).toEqual({ metadata: 'env_local', objectql: 'env_local' }); |
| 89 | + }, BOOT_TIMEOUT); |
| 90 | + |
| 91 | + it('OS_ENVIRONMENT_ID still overrides the default', async () => { |
| 92 | + process.env.OS_ENVIRONMENT_ID = 'env_from_the_environment'; |
| 93 | + const stack = await createStandaloneStack({ databaseUrl: 'memory://standalone-envid-env' }); |
| 94 | + expect(stampedIds(stack.plugins)).toEqual({ |
| 95 | + metadata: 'env_from_the_environment', |
| 96 | + objectql: 'env_from_the_environment', |
| 97 | + }); |
| 98 | + }, BOOT_TIMEOUT); |
| 99 | + |
| 100 | + it('an explicit `cfg.environmentId` still outranks OS_ENVIRONMENT_ID', async () => { |
| 101 | + process.env.OS_ENVIRONMENT_ID = 'env_from_the_environment'; |
| 102 | + const stack = await createStandaloneStack({ |
| 103 | + environmentId: 'env_from_the_config', |
| 104 | + databaseUrl: 'memory://standalone-envid-cfg', |
| 105 | + }); |
| 106 | + expect(stampedIds(stack.plugins)).toEqual({ |
| 107 | + metadata: 'env_from_the_config', |
| 108 | + objectql: 'env_from_the_config', |
| 109 | + }); |
| 110 | + }, BOOT_TIMEOUT); |
| 111 | +}); |
0 commit comments