Skip to content
Draft
Show file tree
Hide file tree
Changes from 27 commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
4a41855
test(planning): define atomic task completion transition
seonghobae Sep 10, 2026
f1dbdfb
test(planning): run task completion transition canary
seonghobae Sep 10, 2026
a8514f0
feat(planning): add atomic task completion application service
seonghobae Sep 10, 2026
2a653b0
chore(planning): retire task completion transition canary
seonghobae Sep 10, 2026
dbfdf75
test(planning): require retry-stable completion evidence
seonghobae Sep 10, 2026
7a01309
test(planning): rerun task completion retry canary
seonghobae Sep 10, 2026
274b0ae
fix(planning): preserve first task completion instant on retry
seonghobae Sep 10, 2026
317a280
chore(planning): retire task completion retry canary
seonghobae Sep 10, 2026
5e0d1a2
test(planning): execute task completion against postgres
seonghobae Sep 10, 2026
d15a62b
ci(planning): verify postgres completion contract
seonghobae Sep 10, 2026
b08478a
fix(planning): bound completion by durable creation time
seonghobae Sep 10, 2026
3263479
test(planning): prove clock-skew completion repair
seonghobae Sep 10, 2026
4667d73
ci(planning): retire completion verifier
seonghobae Sep 10, 2026
c41cca1
test(planning): close completion evidence edge cases
seonghobae Sep 10, 2026
cb3ccf8
ci(planning): verify completion edge coverage
seonghobae Sep 10, 2026
3208d65
ci(planning): repair and verify completion edge coverage
seonghobae Sep 10, 2026
5b9171a
test(planning): type malformed completion evidence fixtures
seonghobae Sep 10, 2026
21c9d51
test(planning): verify completion evidence edges
github-actions[bot] Sep 10, 2026
17132be
test(planning): reject hostile task completion evidence
seonghobae Sep 10, 2026
3a2c686
ci(planning): verify hostile task completion evidence
seonghobae Sep 10, 2026
b732a74
fix(planning): bound hostile completion persistence evidence
seonghobae Sep 10, 2026
dbeb282
style(planning): canonicalize completion persistence boundary
seonghobae Sep 10, 2026
4e022a4
ci(planning): diagnose canonical completion formatting
seonghobae Sep 10, 2026
dff1e9b
style(planning): apply hosted Prettier output
seonghobae Sep 10, 2026
f450755
ci(planning): retire hostile completion verifier
seonghobae Sep 10, 2026
12c926c
ci(planning): verify final completion persistence boundary
seonghobae Sep 10, 2026
81602d6
ci(planning): retire final completion verifier
seonghobae Sep 10, 2026
2db7f2c
test(planning): add bounded revoked-rejection verifier
seonghobae Sep 10, 2026
dc31c7b
test(planning): reject revoked persistence rejections
seonghobae Sep 10, 2026
b1f5ff2
fix(planning): bound revoked persistence rejections
seonghobae Sep 10, 2026
cf6085b
chore(planning): retire revoked-rejection verifier
seonghobae Sep 10, 2026
4b6065a
test(planning): add bounded task-completion SQL snapshot verifier
seonghobae Sep 15, 2026
6623c08
test(planning): prove SQL row envelope snapshot boundary
seonghobae Sep 15, 2026
4a2c5e7
chore(stack): adopt current task completion chronology parent
seonghobae Sep 15, 2026
9a0583a
fix(planning): snapshot SQL completion rows once
seonghobae Sep 15, 2026
b735f9e
fix(ci): bootstrap pnpm after Node setup
seonghobae Sep 16, 2026
b632c3a
test(ci): scope task completion snapshot verifier
seonghobae Sep 16, 2026
22e8685
test(ci): retire task completion snapshot verifier
seonghobae Sep 16, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
createPlanningRuntime,
type PlanningRuntime,
} from './planning-runtime';
import { PostgresTaskCompletionRepository } from './task-completion';
import {
TodayIdempotencyConflictError,
TodayRevisionConflictError,
Expand All @@ -37,6 +38,9 @@ async function applyMigrations(pool: Pool): Promise<void> {
'0001_initial_planning.sql',
'0002_durable_repository_contract.sql',
'0003_durable_today_sync.sql',
'0004_data_rights_erasure_receipts.sql',
'0005_task_completion_chronology.sql',
'0006_validate_task_completion_chronology.sql',
]) {
const sql = await readFile(
resolve(__dirname, '../migrations', migration),
Expand Down Expand Up @@ -92,6 +96,7 @@ describeWithPostgres('PostgreSQL Planning repository integration', () => {
`TRUNCATE
planning.today_idempotency_records,
planning.today_aggregates,
planning.data_rights_erasure_receipts,
planning.tasks,
planning.projects,
planning.goals`,
Expand Down Expand Up @@ -259,6 +264,71 @@ describeWithPostgres('PostgreSQL Planning repository integration', () => {
).resolves.toEqual([]);
});

it('executes retry-stable task completion chronology in PostgreSQL', async () => {
const workspaceId = randomUUID();
const runtime = createRuntime();
const goal = await runtime.service.createGoal(workspaceId, {
title: 'Completion chronology goal',
});
const project = await runtime.service.createProject(workspaceId, {
goalId: goal.id,
title: 'Completion chronology project',
});
const task = await runtime.service.createTask(workspaceId, {
projectId: project.id,
title: 'Completion chronology task',
});
const repository = new PostgresTaskCompletionRepository({
async query<Row>(text: string, values: readonly unknown[]) {
const result = await administrativePool.query(text, [...values]);
return { rows: result.rows as Row[] };
},
});
const taskCreatedAt = new Date(task.createdAt);
const skewedFirstRequestAt = new Date(
taskCreatedAt.getTime() - 60_000,
).toISOString();
const retriedAt = new Date(taskCreatedAt.getTime() + 60_000).toISOString();
const resumedCompletedAt = new Date(
taskCreatedAt.getTime() + 120_000,
).toISOString();

const first = await repository.transitionTaskCompletion(
workspaceId,
task.id,
{ status: 'done', completedAt: skewedFirstRequestAt },
);
const retry = await repository.transitionTaskCompletion(
workspaceId,
task.id,
{ status: 'done', completedAt: retriedAt },
);
const reopened = await repository.transitionTaskCompletion(
workspaceId,
task.id,
{ status: 'todo', completedAt: null },
);
const recompleted = await repository.transitionTaskCompletion(
workspaceId,
task.id,
{ status: 'done', completedAt: resumedCompletedAt },
);

expect(first?.completedAt).toBe(task.createdAt);
expect(retry?.completedAt).toBe(task.createdAt);
expect(reopened).toMatchObject({ status: 'todo', completedAt: null });
expect(recompleted?.completedAt).toBe(resumedCompletedAt);

await expect(
administrativePool.query(
`UPDATE planning.tasks
SET status = 'todo', completed_at = $2::timestamptz
WHERE id = $1`,
[task.id, resumedCompletedAt],
),
).rejects.toMatchObject({ code: '23514' });
});

it('persists Today across restarts while isolating workspaces', async () => {
const workspaceId = randomUUID();
const otherWorkspaceId = randomUUID();
Expand Down
125 changes: 125 additions & 0 deletions apps/planning-service/src/task-completion-hostile-evidence.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
import { describe, expect, it } from 'vitest';
import {
PostgresTaskCompletionRepository,
TaskCompletionPersistenceError,
type TaskCompletionEvidence,
type TaskCompletionRepository,
TaskCompletionService,
type TaskCompletionSqlClient,
type TaskCompletionSqlQueryResult,
} from './task-completion';

const WORKSPACE_ID = '11111111-1111-4111-8111-111111111111';
const TASK_ID = '44444444-4444-4444-8444-444444444444';
const COMPLETED_AT = '2026-09-10T16:00:00.000Z';

function revokedProxy<T extends object>(value: T): T {
const { proxy, revoke } = Proxy.revocable(value, {});
revoke();
return proxy;
}

async function expectCredentialFreePersistenceFailure(
operation: Promise<unknown>,
): Promise<void> {
const error = await operation.catch((caught: unknown) => caught);
expect(error).toBeInstanceOf(TaskCompletionPersistenceError);
expect(error).toMatchObject({
message: 'Persisted task completion data is invalid',
});
}

describe('task completion hostile persistence evidence', () => {
it('collapses a rejected repository dependency without reflecting backend detail', async () => {
const sentinel = 'password=must-not-escape-task-completion-dependency';
const repository: TaskCompletionRepository = {
async transitionTaskCompletion() {
throw new Error(sentinel);
},
};
const service = new TaskCompletionService(
repository,
() => new Date(COMPLETED_AT),
);

await expectCredentialFreePersistenceFailure(
service.setCompleted(WORKSPACE_ID, TASK_ID, true),
);
});

it('collapses revoked producer evidence before reading authority fields', async () => {
const repository: TaskCompletionRepository = {
async transitionTaskCompletion() {
return revokedProxy({
workspaceId: WORKSPACE_ID,
taskId: TASK_ID,
status: 'done' as const,
completedAt: COMPLETED_AT,
}) as TaskCompletionEvidence;
},
};
const service = new TaskCompletionService(
repository,
() => new Date(COMPLETED_AT),
);

await expectCredentialFreePersistenceFailure(
service.setCompleted(WORKSPACE_ID, TASK_ID, true),
);
});

it('collapses a rejected SQL dependency without reflecting backend detail', async () => {
const sentinel = 'postgres=must-not-escape-task-completion-dependency';
const client: TaskCompletionSqlClient = {
async query<Row>(): Promise<TaskCompletionSqlQueryResult<Row>> {
throw new Error(sentinel);
},
};
const repository = new PostgresTaskCompletionRepository(client);

await expectCredentialFreePersistenceFailure(
repository.transitionTaskCompletion(WORKSPACE_ID, TASK_ID, {
status: 'done',
completedAt: COMPLETED_AT,
}),
);
});

it('collapses revoked SQL result envelopes before reading rows', async () => {
const client: TaskCompletionSqlClient = {
async query<Row>(): Promise<TaskCompletionSqlQueryResult<Row>> {
return revokedProxy({ rows: [] }) as TaskCompletionSqlQueryResult<Row>;
},
};
const repository = new PostgresTaskCompletionRepository(client);

await expectCredentialFreePersistenceFailure(
repository.transitionTaskCompletion(WORKSPACE_ID, TASK_ID, {
status: 'done',
completedAt: COMPLETED_AT,
}),
);
});

it('collapses revoked SQL rows before reading durable authority fields', async () => {
const row = revokedProxy({
workspace_id: WORKSPACE_ID,
id: TASK_ID,
status: 'done',
completed_at: COMPLETED_AT,
});
const client: TaskCompletionSqlClient = {
async query<Row>(): Promise<TaskCompletionSqlQueryResult<Row>> {
return { rows: [row as Row] };
},
};
const repository = new PostgresTaskCompletionRepository(client);

await expectCredentialFreePersistenceFailure(
repository.transitionTaskCompletion(WORKSPACE_ID, TASK_ID, {
status: 'done',
completedAt: COMPLETED_AT,
}),
);
});
});
Loading