Skip to content

Commit a255b6a

Browse files
committed
fix(task-list): require own task fields
1 parent 4bd818b commit a255b6a

4 files changed

Lines changed: 99 additions & 12 deletions

File tree

packages/task-list/src/backends/markdown-dir.test.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,35 @@ Body`
187187
expect((task.metadata as { owner?: string }).owner).toBeUndefined();
188188
});
189189

190+
it("does not accept inherited task state fields", async () => {
191+
const { fs } = createFs({
192+
"/repo/tasks/planning/inherited.md": `---
193+
name: Inherited
194+
---
195+
196+
Body`
197+
});
198+
199+
Object.defineProperty(Object.prototype, "state", {
200+
value: "draft",
201+
configurable: true
202+
});
203+
try {
204+
const taskList = await markdownDirBackend({
205+
path: "/repo/tasks",
206+
defaults: { metadata: {} },
207+
create: false,
208+
fs
209+
});
210+
211+
await expect(taskList.list("planning").get("inherited")).rejects.toThrow(
212+
new MalformedTaskError('Malformed task "/repo/tasks/planning/inherited.md": invalid "state".')
213+
);
214+
} finally {
215+
delete (Object.prototype as Record<string, unknown>).state;
216+
}
217+
});
218+
190219
it("preserves proto-named metadata through create, update, and fire", async () => {
191220
const { fs } = createFs();
192221
const taskList = await openTaskList({

packages/task-list/src/backends/markdown-dir.ts

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -233,15 +233,15 @@ function assertValidTaskRecord(
233233
filePath: string,
234234
validStates: ReadonlySet<string>
235235
): void {
236-
if ("$schema" in frontmatter && frontmatter.$schema !== TASK_SCHEMA_ID) {
236+
if (hasOwnTaskField(frontmatter, "$schema") && frontmatter.$schema !== TASK_SCHEMA_ID) {
237237
throw malformedTask(filePath, "$schema");
238238
}
239239

240-
if ("kind" in frontmatter && frontmatter.kind !== TASK_KIND) {
240+
if (hasOwnTaskField(frontmatter, "kind") && frontmatter.kind !== TASK_KIND) {
241241
throw malformedTask(filePath, "kind");
242242
}
243243

244-
if ("version" in frontmatter) {
244+
if (hasOwnTaskField(frontmatter, "version")) {
245245
if (
246246
typeof frontmatter.version !== "number" ||
247247
!Number.isInteger(frontmatter.version) ||
@@ -251,19 +251,31 @@ function assertValidTaskRecord(
251251
}
252252
}
253253

254-
if (typeof frontmatter.name !== "string" || frontmatter.name.length === 0) {
254+
if (
255+
!hasOwnTaskField(frontmatter, "name") ||
256+
typeof frontmatter.name !== "string" ||
257+
frontmatter.name.length === 0
258+
) {
255259
throw malformedTask(filePath, "name");
256260
}
257261

258-
if (typeof frontmatter.state !== "string" || !validStates.has(frontmatter.state)) {
262+
if (
263+
!hasOwnTaskField(frontmatter, "state") ||
264+
typeof frontmatter.state !== "string" ||
265+
!validStates.has(frontmatter.state)
266+
) {
259267
throw malformedTask(filePath, "state");
260268
}
261269

262-
if ("description" in frontmatter && typeof frontmatter.description !== "string") {
270+
if (hasOwnTaskField(frontmatter, "description") && typeof frontmatter.description !== "string") {
263271
throw malformedTask(filePath, "description");
264272
}
265273
}
266274

275+
function hasOwnTaskField(frontmatter: TaskRecord, key: string): boolean {
276+
return Object.prototype.hasOwnProperty.call(frontmatter, key);
277+
}
278+
267279
function reservedFrontmatterKeys(mode: BackendDeps["frontmatterMode"]): ReadonlySet<string> {
268280
return mode === "passthrough" ? PASSTHROUGH_RESERVED_FRONTMATTER_KEYS : RESERVED_FRONTMATTER_KEYS;
269281
}

packages/task-list/src/backends/yaml-file.test.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,40 @@ describe("yamlFileBackend", () => {
191191
await expect(rawFs.readFile("/repo/tasks.yaml", "utf8")).resolves.toContain("__proto__:");
192192
});
193193

194+
it("does not accept inherited task state fields", async () => {
195+
const { fs } = createFs({
196+
"/repo/tasks.yaml": [
197+
"$schema: https://poe-platform.github.io/poe-code/schemas/task-list/store.schema.json",
198+
"kind: task-store",
199+
"version: 1",
200+
"lists:",
201+
" planning:",
202+
" inherited:",
203+
" name: Inherited",
204+
""
205+
].join("\n")
206+
});
207+
208+
Object.defineProperty(Object.prototype, "state", {
209+
value: "draft",
210+
configurable: true
211+
});
212+
try {
213+
const taskList = await yamlFileBackend({
214+
path: "/repo/tasks.yaml",
215+
defaults: { metadata: {} },
216+
create: false,
217+
fs
218+
});
219+
220+
await expect(taskList.list("planning").get("inherited")).rejects.toThrow(
221+
new MalformedTaskError('Malformed task "planning/inherited": invalid "state".')
222+
);
223+
} finally {
224+
delete (Object.prototype as Record<string, unknown>).state;
225+
}
226+
});
227+
194228
it("sets an absolute sourcePath when reading tasks", async () => {
195229
const { fs } = createFs({
196230
"/repo/tasks.yaml": [

packages/task-list/src/backends/yaml-file.ts

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -276,15 +276,15 @@ function assertValidTaskRecord(
276276
throw malformedTask(list, id, "task");
277277
}
278278

279-
if ("$schema" in taskRecord && taskRecord.$schema !== TASK_SCHEMA_ID) {
279+
if (hasOwnTaskField(taskRecord, "$schema") && taskRecord.$schema !== TASK_SCHEMA_ID) {
280280
throw malformedTask(list, id, "$schema");
281281
}
282282

283-
if ("kind" in taskRecord && taskRecord.kind !== TASK_KIND) {
283+
if (hasOwnTaskField(taskRecord, "kind") && taskRecord.kind !== TASK_KIND) {
284284
throw malformedTask(list, id, "kind");
285285
}
286286

287-
if ("version" in taskRecord) {
287+
if (hasOwnTaskField(taskRecord, "version")) {
288288
if (
289289
typeof taskRecord.version !== "number" ||
290290
!Number.isInteger(taskRecord.version) ||
@@ -294,19 +294,31 @@ function assertValidTaskRecord(
294294
}
295295
}
296296

297-
if (typeof taskRecord.name !== "string" || taskRecord.name.length === 0) {
297+
if (
298+
!hasOwnTaskField(taskRecord, "name") ||
299+
typeof taskRecord.name !== "string" ||
300+
taskRecord.name.length === 0
301+
) {
298302
throw malformedTask(list, id, "name");
299303
}
300304

301-
if (typeof taskRecord.state !== "string" || !validStates.has(taskRecord.state)) {
305+
if (
306+
!hasOwnTaskField(taskRecord, "state") ||
307+
typeof taskRecord.state !== "string" ||
308+
!validStates.has(taskRecord.state)
309+
) {
302310
throw malformedTask(list, id, "state");
303311
}
304312

305-
if ("description" in taskRecord && typeof taskRecord.description !== "string") {
313+
if (hasOwnTaskField(taskRecord, "description") && typeof taskRecord.description !== "string") {
306314
throw malformedTask(list, id, "description");
307315
}
308316
}
309317

318+
function hasOwnTaskField(taskRecord: TaskRecord, key: string): boolean {
319+
return Object.prototype.hasOwnProperty.call(taskRecord, key);
320+
}
321+
310322
function validateStoreEntries(
311323
store: StoreRecord,
312324
filePath: string,

0 commit comments

Comments
 (0)