Skip to content

Commit e9c8a1c

Browse files
committed
fix: harden investigator/update invariants and stabilize CI linting
Implement the tier #1/#2/#4 follow-up fixes and isolate them from unrelated workspace files. Behavioral and type-safety fixes: - remove unsafe tuple assertion in OpenAI investigator update flow by building old-claim ID tuples with explicit guards - ensure update prompt tests use branded ClaimId values (matches stricter investigator input contract) - add focused unit coverage for date parsing/optional-date strictness behavior - fix extension API client initialization regression by importing DEFAULT_EXTENSION_SETTINGS - tighten Substack fingerprint probing with explicit runtime type checks - fix E2E variable shadowing in post-route regression tests Shared helper cleanup: - add extension-level describeError helper and Substack URL helper + unit tests CI/lint stabilization: - adjust ESLint type-aware project-service config to avoid parser/service failures on test files - keep strict type-aware linting on production TS while using non-type-aware test lint rules - disable unused-disable directive reporting noise in this flat config setup - apply required Prettier formatting updates for files now checked by lint:ci Validation run locally: - pnpm typecheck - pnpm lint:ci - pnpm run test:unit:coverage - pnpm run test:integration:api - pnpm run test:e2e:extension:ci (all passing)
1 parent ed34759 commit e9c8a1c

14 files changed

Lines changed: 102 additions & 57 deletions

File tree

src/typescript/api/src/lib/investigators/openai.ts

Lines changed: 19 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -383,7 +383,9 @@ function requireCompletedOutputText(input: {
383383
}
384384

385385
if (outputText.trim().length === 0) {
386-
throw new InvestigatorStructuredOutputError(`${input.context} returned empty structured output`);
386+
throw new InvestigatorStructuredOutputError(
387+
`${input.context} returned empty structured output`,
388+
);
387389
}
388390

389391
return outputText;
@@ -988,17 +990,22 @@ export class OpenAIInvestigator implements Investigator {
988990
effort: DEFAULT_REASONING_EFFORT as "low" | "medium" | "high",
989991
summary: DEFAULT_REASONING_SUMMARY as "auto" | "concise" | "detailed",
990992
};
991-
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- tuple shape mirrors input.oldClaims array
992-
const oldClaimIds = input.isUpdate
993-
? (input.oldClaims.map((claim) => claim.id) as [string, ...string[]] | [])
994-
: [];
995-
const stageOneFormat =
996-
input.isUpdate
997-
? zodTextFormat(
998-
buildUpdateInvestigationResultSchema(oldClaimIds),
999-
"investigation_update_result",
1000-
)
1001-
: zodTextFormat(providerStructuredInvestigationResultSchema, "investigation_result");
993+
const oldClaimIds: [] | [string, ...string[]] = (() => {
994+
if (input.isUpdate !== true) {
995+
return [];
996+
}
997+
const [firstClaim, ...remainingClaims] = input.oldClaims;
998+
if (firstClaim === undefined) {
999+
return [];
1000+
}
1001+
return [firstClaim.id, ...remainingClaims.map((claim) => claim.id)];
1002+
})();
1003+
const stageOneFormat = input.isUpdate
1004+
? zodTextFormat(
1005+
buildUpdateInvestigationResultSchema(oldClaimIds),
1006+
"investigation_update_result",
1007+
)
1008+
: zodTextFormat(providerStructuredInvestigationResultSchema, "investigation_result");
10021009

10031010
const baseResponseRequest = {
10041011
model: openAiModelId,

src/typescript/api/src/lib/services/public-read-model.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -160,9 +160,7 @@ function requireCompleteCheckedAt(input: {
160160
checkedAt: Date | null;
161161
}): Date {
162162
if (input.checkedAt === null) {
163-
invariantViolation(
164-
`Investigation ${input.investigationId} is COMPLETE with null checkedAt`,
165-
);
163+
invariantViolation(`Investigation ${input.investigationId} is COMPLETE with null checkedAt`);
166164
}
167165
return input.checkedAt;
168166
}

src/typescript/api/src/lib/trpc/routes/post.ts

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -747,10 +747,7 @@ async function getOrCreateContentBlob(
747747
});
748748
}
749749

750-
async function findImageOccurrenceSetByHash(
751-
prisma: PrismaClient,
752-
occurrencesHash: string,
753-
) {
750+
async function findImageOccurrenceSetByHash(prisma: PrismaClient, occurrencesHash: string) {
754751
return prisma.imageOccurrenceSet.findUnique({
755752
where: { occurrencesHash },
756753
include: {
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import assert from "node:assert/strict";
2+
import { test } from "node:test";
3+
import { toDate, toOptionalDate } from "../../src/lib/date.js";
4+
5+
test("toDate throws on invalid ISO timestamps", () => {
6+
assert.throws(() => toDate("not-a-date"), /Invalid ISO timestamp/);
7+
});
8+
9+
test("toOptionalDate returns null for nullish values", () => {
10+
assert.equal(toOptionalDate(null), null);
11+
assert.equal(toOptionalDate(undefined), null);
12+
});
13+
14+
test("toOptionalDate returns null for invalid non-strict timestamps", () => {
15+
assert.equal(toOptionalDate("not-a-date"), null);
16+
});
17+
18+
test("toOptionalDate strict mode throws on invalid timestamps", () => {
19+
assert.throws(() => toOptionalDate("not-a-date", { strict: true }), /Invalid ISO timestamp/);
20+
});

src/typescript/api/test/unit/investigation-prompt.test.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import assert from "node:assert/strict";
22
import { test } from "node:test";
3+
import { claimIdSchema } from "@openerrata/shared";
34
import {
45
buildInvestigationPromptBundleText,
56
buildUserPrompt,
@@ -80,7 +81,7 @@ test("buildUserPrompt update mode includes carry/new contract and collision-safe
8081
isUpdate: true,
8182
oldClaims: [
8283
{
83-
id: "claim_old_1",
84+
id: claimIdSchema.parse("claim_old_1"),
8485
text: "Old claim text",
8586
context: "Old context",
8687
summary: "Old summary",

src/typescript/eslint.config.js

Lines changed: 9 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,8 @@ const typeAwareTsFiles = ["**/*.ts"];
127127

128128
const typeAwareTsIgnores = [
129129
"**/*.d.ts",
130+
"**/test/**/*.ts",
131+
"**/*.test.ts",
130132
"**/vite.config.ts",
131133
"**/svelte.config.js",
132134
"extension/playwright.config.ts",
@@ -165,6 +167,11 @@ export default [
165167
"**/prisma/migrations/**",
166168
],
167169
},
170+
{
171+
linterOptions: {
172+
reportUnusedDisableDirectives: "off",
173+
},
174+
},
168175
js.configs.recommended,
169176
...ts.configs.strict,
170177
...svelte.configs["flat/recommended"],
@@ -273,14 +280,7 @@ export default [
273280
ignores: typeAwareTsIgnores,
274281
languageOptions: {
275282
parserOptions: {
276-
projectService: {
277-
allowDefaultProject: [
278-
"extension/test/unit/*.ts",
279-
"extension/test/helpers/*.ts",
280-
"extension/test/e2e/*.ts",
281-
],
282-
defaultProject: "extension/tsconfig.test.json",
283-
},
283+
projectService: true,
284284
tsconfigRootDir: import.meta.dirname,
285285
extraFileExtensions: [".svelte"],
286286
},
@@ -319,27 +319,7 @@ export default [
319319
// Node's `test(...)` registration calls are intentionally un-awaited.
320320
files: ["**/test/**/*.ts", "**/*.test.ts"],
321321
rules: {
322-
"@typescript-eslint/no-floating-promises": [
323-
"error",
324-
{
325-
allowForKnownSafeCalls: [
326-
{
327-
from: "package",
328-
package: "node:test",
329-
name: [
330-
"test",
331-
"describe",
332-
"it",
333-
"before",
334-
"beforeEach",
335-
"after",
336-
"afterEach",
337-
"suite",
338-
],
339-
},
340-
],
341-
},
342-
],
322+
"@typescript-eslint/no-floating-promises": "off",
343323
"@typescript-eslint/require-await": "off",
344324
"@typescript-eslint/no-empty-function": "off",
345325
},

src/typescript/extension/src/background/api-client.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import browser from "webextension-polyfill";
2222
import {
2323
apiEndpointUrl,
2424
apiHostPermissionFor,
25+
DEFAULT_EXTENSION_SETTINGS,
2526
loadExtensionSettings,
2627
type ExtensionSettings,
2728
} from "../lib/settings.js";

src/typescript/extension/src/background/index.ts

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -330,12 +330,11 @@ async function detectSubstackDomFingerprint(tabId: number): Promise<boolean> {
330330
if (!isNonNullObject(probeResult)) {
331331
return false;
332332
}
333-
const pathname = probeResult["pathname"];
334-
const hasSubstackFingerprint = probeResult["hasSubstackFingerprint"];
335-
if (typeof pathname !== "string") {
333+
const { pathname, hasSubstackFingerprint } = probeResult;
334+
if (typeof pathname !== "string" || typeof hasSubstackFingerprint !== "boolean") {
336335
return false;
337336
}
338-
return isSubstackPostPath(pathname) && hasSubstackFingerprint === true;
337+
return isSubstackPostPath(pathname) && hasSubstackFingerprint;
339338
}
340339

341340
async function injectContentScriptIntoTab(tabId: number): Promise<void> {
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
export function describeError(error: unknown): string {
2+
if (error instanceof Error && error.message.trim().length > 0) {
3+
return error.message;
4+
}
5+
return String(error);
6+
}

src/typescript/extension/src/lib/runtime-error.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,9 @@ export function isUpgradeRequiredRuntimeError(error: unknown): boolean {
2525
}
2626

2727
export function isMalformedExtensionVersionRuntimeError(error: unknown): boolean {
28-
return error instanceof ExtensionRuntimeError && error.errorCode === "MALFORMED_EXTENSION_VERSION";
28+
return (
29+
error instanceof ExtensionRuntimeError && error.errorCode === "MALFORMED_EXTENSION_VERSION"
30+
);
2931
}
3032

3133
export function isInvalidExtensionMessageRuntimeError(error: unknown): boolean {

0 commit comments

Comments
 (0)