|
| 1 | +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. |
| 2 | + |
| 3 | +/** |
| 4 | + * This package spells the standalone-action owner-key ladder ONCE (#14422). |
| 5 | + * |
| 6 | + * `ObjectQLPlugin` carried a private `actionObjectKey` that repeated |
| 7 | + * {@link standaloneActionOwnerKey}'s three rungs, and the only thing holding |
| 8 | + * the two equal was a sentence in each docblock. It had already drifted in the |
| 9 | + * one way a copy can drift without any test noticing: the plugin's terminal |
| 10 | + * rung returned the bare literal `'global'` while the canonical helper returns |
| 11 | + * `GLOBAL_ACTION_OBJECT_KEY`. Equal in value on the day it was measured, and |
| 12 | + * silently different the first time that constant moves. |
| 13 | + * |
| 14 | + * `@objectstack/runtime` carries the matching weld for its own copy |
| 15 | + * (`action-owner-key-single-source.test.ts` there). This one is scoped to this |
| 16 | + * package's source so it stays a package-local test input. |
| 17 | + */ |
| 18 | + |
| 19 | +import { readFileSync, readdirSync } from 'node:fs'; |
| 20 | +import { dirname, join } from 'node:path'; |
| 21 | +import { describe, it, expect } from 'vitest'; |
| 22 | +import { GLOBAL_ACTION_OBJECT_KEY, standaloneActionOwnerKey } from './action-governance.js'; |
| 23 | + |
| 24 | +/** Rung 1 exactly as `action-governance.ts` writes it. */ |
| 25 | +const LADDER_RUNG_1 = "typeof action?.objectName === 'string' && action.objectName.length > 0"; |
| 26 | +/** Rung 2, likewise. */ |
| 27 | +const LADDER_RUNG_2 = "typeof action?.object === 'string' && action.object.length > 0"; |
| 28 | + |
| 29 | +/** |
| 30 | + * This package's `src` directory, located from the test file's own path via |
| 31 | + * vitest's runner state rather than `import.meta.url`: this package builds to |
| 32 | + * CommonJS, where `import.meta` is a TS1470 that would bill the TEST_DEBT |
| 33 | + * ledger for a config error saying nothing about this test. |
| 34 | + */ |
| 35 | +function srcDir(): string { |
| 36 | + const testPath = expect.getState().testPath; |
| 37 | + if (!testPath) { |
| 38 | + throw new Error('vitest did not report a testPath — the #14422 weld cannot locate this package.'); |
| 39 | + } |
| 40 | + return dirname(testPath); |
| 41 | +} |
| 42 | + |
| 43 | +function nonTestSources(): Array<{ file: string; text: string }> { |
| 44 | + const dir = srcDir(); |
| 45 | + const files = readdirSync(dir).filter((f) => f.endsWith('.ts') && !f.endsWith('.test.ts')); |
| 46 | + if (files.length === 0) { |
| 47 | + throw new Error(`No sources found under ${dir} — the #14422 weld would pass vacuously. Fix this scan.`); |
| 48 | + } |
| 49 | + return files.map((file) => ({ file, text: readFileSync(join(dir, file), 'utf8') })); |
| 50 | +} |
| 51 | + |
| 52 | +describe('standalone-action owner key — one spelling in @objectstack/objectql (#14422)', () => { |
| 53 | + it('writes each ladder rung in exactly one file, and that file is action-governance.ts', () => { |
| 54 | + const sources = nonTestSources(); |
| 55 | + // Anti-vacuity: the scan must be able to SEE the canonical spelling. |
| 56 | + // A rung constant that matched nothing would make both counts zero and |
| 57 | + // the assertion below green for the wrong reason. |
| 58 | + const canonical = sources.find((s) => s.file === 'action-governance.ts'); |
| 59 | + expect(canonical, 'action-governance.ts is missing from the scan').toBeDefined(); |
| 60 | + expect(canonical!.text).toContain(LADDER_RUNG_1); |
| 61 | + expect(canonical!.text).toContain(LADDER_RUNG_2); |
| 62 | + |
| 63 | + for (const rung of [LADDER_RUNG_1, LADDER_RUNG_2]) { |
| 64 | + const carriers = sources.filter((s) => s.text.includes(rung)).map((s) => s.file); |
| 65 | + expect(carriers, `ladder rung re-inlined: ${rung}`).toEqual(['action-governance.ts']); |
| 66 | + } |
| 67 | + }); |
| 68 | + |
| 69 | + it('leaves no private `actionObjectKey` behind on the plugin', () => { |
| 70 | + const plugin = nonTestSources().find((s) => s.file === 'plugin.ts'); |
| 71 | + expect(plugin, 'plugin.ts is missing from the scan').toBeDefined(); |
| 72 | + expect(plugin!.text).not.toContain('actionObjectKey'); |
| 73 | + // Positive control for the negative above: the plugin does still derive |
| 74 | + // owner keys — it just does it through the canonical helper now. |
| 75 | + expect(plugin!.text).toContain('standaloneActionOwnerKey('); |
| 76 | + }); |
| 77 | + |
| 78 | + it('terminates the ladder on the constant, never on a bare literal', () => { |
| 79 | + expect(standaloneActionOwnerKey({})).toBe(GLOBAL_ACTION_OBJECT_KEY); |
| 80 | + const canonical = nonTestSources().find((s) => s.file === 'action-governance.ts')!.text; |
| 81 | + const body = canonical.match(/export function standaloneActionOwnerKey\([^)]*\): string \{([\s\S]*?)\n\}/); |
| 82 | + if (!body) { |
| 83 | + throw new Error( |
| 84 | + 'Could not locate `standaloneActionOwnerKey` in action-governance.ts. ' |
| 85 | + + 'The #14422 weld cannot verify itself — fix this parse rather than deleting it.', |
| 86 | + ); |
| 87 | + } |
| 88 | + expect(body[1]).toContain('return GLOBAL_ACTION_OBJECT_KEY;'); |
| 89 | + expect(body[1]).not.toContain("'global'"); |
| 90 | + }); |
| 91 | +}); |
0 commit comments